From cdfd678c7a10de756bc1d48b4c3214d337fd512f Mon Sep 17 00:00:00 2001 From: Kartheek Palla Date: Tue, 7 Nov 2023 17:01:19 +0530 Subject: [PATCH 01/47] Issue KN-953 fix: vulnerabilities fixes --- pom.xml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/pom.xml b/pom.xml index da71cc7..3713b3e 100644 --- a/pom.xml +++ b/pom.xml @@ -2,7 +2,7 @@ 4.0.0 org.sunbird cloud-store-sdk_2.12 - 1.4.6 + 1.4.7 jar Cloud Store SDK cloud-store-sdk provides client APIs to handle cloud store (AWS S3, Azure Blobstore, GCP, OCI). @@ -75,7 +75,7 @@ org.apache.hadoop hadoop-azure - 2.7.3 + 3.3.2 com.typesafe @@ -96,17 +96,17 @@ com.fasterxml.jackson.core jackson-databind - 2.7.8 + 2.12.7.1 com.fasterxml.jackson.module - jackson-module-scala_${scala.maj.version}.0-RC1 - 2.7.8 + jackson-module-scala_${scala.maj.version} + 2.9.8 org.apache.tika tika-core - 1.18 + 2.9.0 From 70510ede8b68393769ced7aacd6c2496699a0521 Mon Sep 17 00:00:00 2001 From: Kartheek Palla Date: Wed, 8 Nov 2023 11:07:12 +0530 Subject: [PATCH 02/47] Issue KN-953 fix: vulnerabilities fixes --- pom.xml | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 3713b3e..94dcc80 100644 --- a/pom.xml +++ b/pom.xml @@ -75,8 +75,19 @@ org.apache.hadoop hadoop-azure - 3.3.2 + 2.7.3 + + + org.apache.hadoop + hadoop-common + + + + org.apache.hadoop + hadoop-common + 3.3.4 + com.typesafe config From 86b06d7f9ad5aa672d25c797ac1edd1056c06965 Mon Sep 17 00:00:00 2001 From: Mahesh Kumar Gangula Date: Wed, 8 Nov 2023 14:56:03 +0530 Subject: [PATCH 03/47] Issue KN-953 feat: Jackson upgrade to 2.13.5 --- pom.xml | 24 +++++++++++++++---- .../cloud/storage/util/JSONUtils.scala | 5 +++- 2 files changed, 24 insertions(+), 5 deletions(-) diff --git a/pom.xml b/pom.xml index 94dcc80..43a843d 100644 --- a/pom.xml +++ b/pom.xml @@ -71,7 +71,13 @@ com.microsoft.azure azure-storage 5.0.0 - + + + jackson-core + com.fasterxml.jackson.core + + + org.apache.hadoop hadoop-azure @@ -81,12 +87,22 @@ org.apache.hadoop hadoop-common - + + jackson-core + com.fasterxml.jackson.core + + org.apache.hadoop hadoop-common 3.3.4 + + + jackson-databind + com.fasterxml.jackson.core + + com.typesafe @@ -107,12 +123,12 @@ com.fasterxml.jackson.core jackson-databind - 2.12.7.1 + 2.13.5 com.fasterxml.jackson.module jackson-module-scala_${scala.maj.version} - 2.9.8 + 2.13.5 org.apache.tika diff --git a/src/main/scala/org/sunbird/cloud/storage/util/JSONUtils.scala b/src/main/scala/org/sunbird/cloud/storage/util/JSONUtils.scala index 9dc999e..364d228 100644 --- a/src/main/scala/org/sunbird/cloud/storage/util/JSONUtils.scala +++ b/src/main/scala/org/sunbird/cloud/storage/util/JSONUtils.scala @@ -6,15 +6,18 @@ import com.fasterxml.jackson.core.`type`.TypeReference import com.fasterxml.jackson.databind.DeserializationFeature import com.fasterxml.jackson.databind.ObjectMapper import com.fasterxml.jackson.module.scala.DefaultScalaModule +import com.fasterxml.jackson.annotation.JsonInclude import com.fasterxml.jackson.annotation.JsonInclude.Include +import com.fasterxml.jackson.annotation.JsonInclude.Value import com.fasterxml.jackson.databind.SerializationFeature + object JSONUtils { @transient val mapper = new ObjectMapper(); mapper.registerModule(DefaultScalaModule); mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); - mapper.configure(SerializationFeature.WRITE_NULL_MAP_VALUES, false); + mapper.configOverride(Map.getClass).setInclude(Value.construct(JsonInclude.Include.NON_NULL, JsonInclude.Include.NON_NULL)) mapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false); mapper.setSerializationInclusion(Include.NON_NULL); From 92a9e000ecdded917c8ccdac3327dbd99071ae4f Mon Sep 17 00:00:00 2001 From: Kartheek Palla Date: Wed, 8 Nov 2023 18:27:45 +0530 Subject: [PATCH 04/47] Issue KN-953 fix: vulnerabilities fixes --- pom.xml | 99 ++++++++++++++++++++++++++++++++++++++++++++------------- 1 file changed, 77 insertions(+), 22 deletions(-) diff --git a/pom.xml b/pom.xml index 43a843d..bf7e58c 100644 --- a/pom.xml +++ b/pom.xml @@ -66,7 +66,27 @@ org.apache.jclouds jclouds-all 2.2.1 + + + com.jamesmurty.utils + java-xmlbuilder + + + org.yaml + snakeyaml + + + + org.yaml + snakeyaml + 2.0 + + + com.jamesmurty.utils + java-xmlbuilder + 1.2 + com.microsoft.azure azure-storage @@ -82,28 +102,64 @@ org.apache.hadoop hadoop-azure 2.7.3 - - - org.apache.hadoop - hadoop-common - - - jackson-core - com.fasterxml.jackson.core - - + + + org.apache.hadoop + hadoop-common + + + jackson-core + com.fasterxml.jackson.core + + - - org.apache.hadoop - hadoop-common - 3.3.4 - - - jackson-databind - com.fasterxml.jackson.core - - - + + org.apache.hadoop + hadoop-common + 3.3.6 + + + jackson-databind + com.fasterxml.jackson.core + + + com.google.protobuf + protobuf-java + + + org.codehaus.jettison + jettison + + + org.xerial.snappy + snappy-java + + + org.apache.avro + avro + + + + + org.apache.avro + avro + 1.11.3 + + + org.xerial.snappy + snappy-java + 1.1.10.4 + + + org.codehaus.jettison + jettison + 1.5.4 + + + com.google.protobuf + protobuf-java + 3.21.7 + com.typesafe config @@ -238,4 +294,3 @@ - From 1153012951c9a9c8d899cbd31545de9652ca3255 Mon Sep 17 00:00:00 2001 From: aimansharief Date: Tue, 6 May 2025 11:52:51 +0530 Subject: [PATCH 05/47] fix: GCP signed url and getUri fix --- pom.xml | 8 +- .../cloud/storage/BaseStorageService.scala | 11 +- .../cloud/storage/IStorageService.scala | 2 +- .../service/GcloudStorageService.scala | 224 +++++++++++++++++- 4 files changed, 237 insertions(+), 8 deletions(-) diff --git a/pom.xml b/pom.xml index bf7e58c..d4b7416 100644 --- a/pom.xml +++ b/pom.xml @@ -2,7 +2,7 @@ 4.0.0 org.sunbird cloud-store-sdk_2.12 - 1.4.7 + 1.4.7.1 jar Cloud Store SDK cloud-store-sdk provides client APIs to handle cloud store (AWS S3, Azure Blobstore, GCP, OCI). @@ -13,6 +13,7 @@ UTF-8 1.1.1 2.12 + 2.16.0 2.12.11 @@ -77,6 +78,11 @@ + + com.google.cloud + google-cloud-storage + ${gcp.storage.version} + org.yaml snakeyaml diff --git a/src/main/scala/org/sunbird/cloud/storage/BaseStorageService.scala b/src/main/scala/org/sunbird/cloud/storage/BaseStorageService.scala index 8743e8f..934393d 100644 --- a/src/main/scala/org/sunbird/cloud/storage/BaseStorageService.scala +++ b/src/main/scala/org/sunbird/cloud/storage/BaseStorageService.scala @@ -141,15 +141,16 @@ trait BaseStorageService extends IStorageService { } } - override def getSignedURLV2(container: String, objectKey: String, ttl: Option[Int] = None, permission: Option[String] = Option("r"), contentType: Option[String] = Option("text/plain")): String = { + override def getSignedURLV2(container: String, objectKey: String, ttl: Option[Int] = None, permission: Option[String] = Option("r"), + contentType: Option[String] = Option("application/octet-stream"), additionalParams: Option[Map[String,String]] = None): String = { if (context.getBlobStore.toString.contains("google")) { - getPutSignedURL(container, objectKey, Option(maxSignedurlTTL), None, contentType) + getPutSignedURL(container, objectKey, Option(maxSignedurlTTL), None, contentType, additionalParams) } else { getSignedURL(container, objectKey, ttl, permission) } } - - def getPutSignedURL(container: String, objectKey: String, ttl: Option[Int] = None, permission: Option[String] = Option("r"), contentType: Option[String] = Option("text/plain")): String = { + def getPutSignedURL(container: String, objectKey: String, ttl: Option[Int] = None, permission: Option[String] = Option("r"), + contentType: Option[String] = Option("application/octet-stream"), additionalParams: Option[Map[String,String]] = None): String = { if (permission.getOrElse("").equalsIgnoreCase("w")) { context.getSigner.signPutBlob(container, blobStore.blobBuilder(objectKey).forSigning().contentLength(maxContentLength).contentType(contentType.get).build(), ttl.getOrElse(maxSignedurlTTL).asInstanceOf[Number].longValue()).getEndpoint.toString } else { @@ -327,4 +328,4 @@ trait BaseStorageService extends IStorageService { context.close() } -} +} \ No newline at end of file diff --git a/src/main/scala/org/sunbird/cloud/storage/IStorageService.scala b/src/main/scala/org/sunbird/cloud/storage/IStorageService.scala index 3c8d450..bc5312f 100644 --- a/src/main/scala/org/sunbird/cloud/storage/IStorageService.scala +++ b/src/main/scala/org/sunbird/cloud/storage/IStorageService.scala @@ -63,7 +63,7 @@ trait IStorageService { * * @return String - The pre-signed url */ - def getSignedURLV2(container: String, objectKey: String, ttl: Option[Int] = None, permission: Option[String] = Option("r"), contentType: Option[String] = Option("text/plain")): String + def getSignedURLV2(container: String, objectKey: String, ttl: Option[Int] = None, permission: Option[String] = Option("r"), contentType: Option[String] = Option("text/plain"), additionalParams: Option[Map[String, String]] = None): String /** * Download file/folder from cloud storage diff --git a/src/main/scala/org/sunbird/cloud/storage/service/GcloudStorageService.scala b/src/main/scala/org/sunbird/cloud/storage/service/GcloudStorageService.scala index c6cceed..33ae4a4 100644 --- a/src/main/scala/org/sunbird/cloud/storage/service/GcloudStorageService.scala +++ b/src/main/scala/org/sunbird/cloud/storage/service/GcloudStorageService.scala @@ -1,5 +1,7 @@ package org.sunbird.cloud.storage.service +import com.google.auth.oauth2.ServiceAccountCredentials +import com.google.cloud.storage.{BlobId, BlobInfo, HttpMethod, Storage, StorageOptions} import com.google.common.io.Files import org.jclouds.ContextBuilder import org.jclouds.blobstore.BlobStoreContext @@ -7,8 +9,12 @@ import org.sunbird.cloud.storage.BaseStorageService import org.sunbird.cloud.storage.Model.Blob import org.sunbird.cloud.storage.exception.StorageServiceException import org.sunbird.cloud.storage.factory.StorageConfig +import org.apache.tika.metadata.HttpHeaders +import org.apache.tika.mime.MimeTypes -import java.io.File +import java.io.{File, FileOutputStream} +import java.util.concurrent.TimeUnit +import scala.collection.JavaConverters._ class GcloudStorageService(config: StorageConfig) extends BaseStorageService { @@ -85,4 +91,220 @@ class GcloudStorageService(config: StorageConfig) extends BaseStorageService { } else throw new StorageServiceException("uri not available for the given prefix: "+ _prefix) } + /** + * Method to get V4 Signed URL when storage is GCP + * @param re + * @return + */ + override def getPutSignedURL(container: String, objectKey: String, ttl: Option[Int], permission: Option[String] = Option("r"), + contentType: Option[String] = Option("application/octet-stream"), additionalParams: Option[Map[String,String]] = None): String = { + if(additionalParams.get == None) { + throw new StorageServiceException("Missing google credentials params.") + } + val properties = additionalParams.get; + // getting credentials + val credentials = ServiceAccountCredentials.fromPkcs8(properties.get("clientId").get, properties.get("clientEmail").get, properties.get("privateKeyPkcs8").get, + properties.get("privateKeyIds").get, new java.util.ArrayList[String]()) + // creating storage options + val storage = StorageOptions.newBuilder.setProjectId(properties.get("projectId").get).setCredentials(credentials).build.getService + // setting header as application/octet-stream (required by google) + val extensionHeaders = Map(HttpHeaders.CONTENT_TYPE -> contentType.getOrElse(MimeTypes.OCTET_STREAM)) + // creating blob info + val blobInfo = BlobInfo.newBuilder(BlobId.of(container, objectKey)).build + // expiry time validation as TTL cannot be greater than 604800 + // expiry time will be set to default value of 604800 if greater than 604800 + val expiryTime = if(ttl.get > maxSignedurlTTL) maxSignedurlTTL else ttl.get + //creating signed url + val url = storage.signUrl(blobInfo, expiryTime, TimeUnit.SECONDS, Storage.SignUrlOption.httpMethod(HttpMethod.PUT), +// Storage.SignUrlOption.withExtHeaders(extensionHeaders.asJava), + Storage.SignUrlOption.withV4Signature); + url.toString; + } + + // Override listObjectKeys to avoid JClouds parsing issues + override def listObjectKeys(container: String, prefix: String): List[String] = { + try { + println(s"Listing objects with prefix '$prefix' in container '$container' using direct HTTP") + + // Use the Google Cloud Storage JSON API + val host = "https://storage.googleapis.com/storage/v1/b" + val url = new java.net.URL(s"${host}/${container}/o?prefix=${java.net.URLEncoder.encode(prefix, "UTF-8")}") + + println(s"Querying API URL: ${url}") + + val connection = url.openConnection().asInstanceOf[java.net.HttpURLConnection] + connection.setRequestMethod("GET") + connection.setRequestProperty("Accept", "application/json") + + val responseCode = connection.getResponseCode + if (responseCode != 200) { + println(s"Failed to list objects. Response code: $responseCode") + return List.empty[String] + } + + // Parse the JSON response + val responseStream = connection.getInputStream + val responseString = scala.io.Source.fromInputStream(responseStream).mkString + responseStream.close() + + // Parse items from the response + val items = parseJsonItems(responseString) + + if (items.isEmpty) { + println("No items found in the response") + List.empty[String] + } else { + println(s"Found ${items.size} objects") + items + } + } catch { + case e: Exception => + println(s"Error listing objects: ${e.getMessage}") + e.printStackTrace() + List.empty[String] + } + } + + // Helper method to parse JSON without external dependencies + private def parseJsonItems(json: String): List[String] = { + try { + // Simple regex-based JSON parsing to extract names + val namePattern = """"name"\s*:\s*"([^"]+)"""".r + val matches = namePattern.findAllMatchIn(json) + matches.map(_.group(1)).toList + } catch { + case e: Exception => + println(s"Error parsing JSON: ${e.getMessage}") + e.printStackTrace() + List.empty[String] + } + } + + override def download(container: String, objectKey: String, localPath: String, isDirectory: Option[Boolean] = Option(false)): Unit = { + try { + if(isDirectory.get) { + println(s"Starting GCP directory download operation for: container=$container, prefix=$objectKey to $localPath") + val objects = listObjectKeys(container, objectKey) + + if (objects.isEmpty) { + println(s"No objects found with prefix $objectKey in container $container") + return + } + + println(s"Found ${objects.size} objects to download from prefix $objectKey") + + // Ensure local directory exists + val downloadDir = new File(localPath) + if (!downloadDir.exists()) { + downloadDir.mkdirs() + } + + var successCount = 0 + var failureCount = 0 + + // Download each object + for (obj <- objects) { + try { + // Calculate the relative path from the prefix + val relativePath = if (obj.startsWith(objectKey)) { + obj.substring(objectKey.length) + } else { + obj + } + + // Create full local path with directories + val fullLocalPath = new File(localPath, relativePath) + val parentDir = fullLocalPath.getParentFile + if (parentDir != null && !parentDir.exists()) { + parentDir.mkdirs() + } + + println(s"Downloading object: $obj to ${fullLocalPath.getAbsolutePath}") + val result = downloadFile(container, obj, fullLocalPath.getAbsolutePath) + + if (result) { + successCount += 1 + } else { + failureCount += 1 + } + } catch { + case e: Exception => + println(s"Error downloading object $obj: ${e.getMessage}") + failureCount += 1 + } + } + + println(s"Directory download completed. Successfully downloaded $successCount files. Failed to download $failureCount files.") + } else { + println(s"Starting GCP file download operation for: container=$container, objectKey=$objectKey to $localPath") + + val fileName = objectKey.split("/").last + val downloadFilePath = new File(localPath, fileName).getAbsolutePath + + downloadFile(container, objectKey, downloadFilePath) + } + } catch { + case e: Exception => + println(s"Error during download: ${e.getMessage}") + e.printStackTrace() + throw new StorageServiceException(s"Failed to download object $objectKey from container $container: ${e.getMessage}", e) + } + } + + /** + * Downloads a single file from GCP storage + * + * @param container The storage container/bucket name + * @param objectKey The object key to download + * @param destinationPath The full path where the file should be saved + * @return true if download was successful, false otherwise + */ + private def downloadFile(container: String, objectKey: String, destinationPath: String): Boolean = { + try { + // Ensure parent directory exists + val downloadFile = new File(destinationPath) + val parentDir = downloadFile.getParentFile + if (parentDir != null && !parentDir.exists()) { + parentDir.mkdirs() + } + + // Handle the download using a direct HTTP approach to avoid JClouds parsing issues + val host = "https://storage.googleapis.com/" + val url = new java.net.URL(s"${host}${container}/${objectKey}") + + println(s"Downloading from URL: ${url}") + + val connection = url.openConnection().asInstanceOf[java.net.HttpURLConnection] + connection.setRequestMethod("GET") + + val responseCode = connection.getResponseCode + if (responseCode != 200) { + println(s"Failed to download $objectKey. Response code: $responseCode") + return false + } + + val inputStream = connection.getInputStream + val outputStream = new FileOutputStream(downloadFile) + + try { + val buffer = new Array[Byte](8192) + var bytesRead = inputStream.read(buffer) + while (bytesRead != -1) { + outputStream.write(buffer, 0, bytesRead) + bytesRead = inputStream.read(buffer) + } + println(s"Successfully downloaded file to ${downloadFile.getAbsolutePath}") + true + } finally { + if (inputStream != null) inputStream.close() + if (outputStream != null) outputStream.close() + } + } catch { + case e: Exception => + println(s"Error downloading object $objectKey: ${e.getMessage}") + e.printStackTrace() + false + } + } + } \ No newline at end of file From 87df2cf00af2c6f42d521a6b936dba0f0b08c741 Mon Sep 17 00:00:00 2001 From: aimansharief Date: Tue, 6 May 2025 12:38:57 +0530 Subject: [PATCH 06/47] fix: GCP getUri fix --- .../cloud/storage/service/GcloudStorageService.scala | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/src/main/scala/org/sunbird/cloud/storage/service/GcloudStorageService.scala b/src/main/scala/org/sunbird/cloud/storage/service/GcloudStorageService.scala index 33ae4a4..6e5a582 100644 --- a/src/main/scala/org/sunbird/cloud/storage/service/GcloudStorageService.scala +++ b/src/main/scala/org/sunbird/cloud/storage/service/GcloudStorageService.scala @@ -80,14 +80,9 @@ class GcloudStorageService(config: StorageConfig) extends BaseStorageService { val blob = getObject(container, prefix, Option(false)) val uri = blob.metadata.get("publicUri") if (!uri.isEmpty) { - if(isDirectory.get){ - throw new StorageServiceException("getUri for directory is not supported for GCP. The given _prefix is incorrect: " + _prefix) - } - else{ - val host = "https://storage.googleapis.com/" - val name = blob.metadata.get("name").get.asInstanceOf[String] - host + container + "/" + name - } + val host = "https://storage.googleapis.com/" + val name = blob.metadata.get("name").get.asInstanceOf[String] + host + container + "/" + name } else throw new StorageServiceException("uri not available for the given prefix: "+ _prefix) } From 08f54c9b7bc1cc8ddcf70d52bd9da01d6f089401 Mon Sep 17 00:00:00 2001 From: aimansharief Date: Wed, 28 May 2025 17:28:09 +0530 Subject: [PATCH 07/47] fix : Update POM configuration to enable publishing to Maven Central --- pom.xml | 38 ++++++++++++++++++-------------------- 1 file changed, 18 insertions(+), 20 deletions(-) diff --git a/pom.xml b/pom.xml index d4b7416..6c5af79 100644 --- a/pom.xml +++ b/pom.xml @@ -2,7 +2,7 @@ 4.0.0 org.sunbird cloud-store-sdk_2.12 - 1.4.7.1 + 1.4.7.2 jar Cloud Store SDK cloud-store-sdk provides client APIs to handle cloud store (AWS S3, Azure Blobstore, GCP, OCI). @@ -31,15 +31,15 @@ - - ossrh - https://oss.sonatype.org/content/repositories/snapshots - - - ossrh - https://oss.sonatype.org/service/local/staging/deploy/maven2/ - - + + central + https://s01.oss.sonatype.org/content/repositories/snapshots + + + central + https://s01.oss.sonatype.org/service/local/staging/deploy/maven2/ + + scm:git:git://github.com/project-sunbird/sunbird-cloud-storage-sdk.git scm:git:ssh://github.com/project-sunbird/sunbird-cloud-storage-sdk.git @@ -287,16 +287,14 @@ - org.sonatype.plugins - nexus-staging-maven-plugin - 1.6.7 - true - - ossrh - https://oss.sonatype.org/ - - - + org.sonatype.central + central-publishing-maven-plugin + 0.7.0 + true + + central + + From 6cf5d6c6d3ab5191dc12e4e943f13030f89965dc Mon Sep 17 00:00:00 2001 From: Mahesh Kumar Gangula Date: Wed, 24 Sep 2025 13:23:34 +0530 Subject: [PATCH 08/47] feat: GCP chunk upload support. Include "chunked" -> "true" in additionalParams to generate the URL to support resumable (chunked) upload of a file. --- .../service/GcloudStorageService.scala | 28 ++++++++-- ...tGcloudStorageServiceGetPutSignedURL.scala | 52 +++++++++++++++++++ 2 files changed, 75 insertions(+), 5 deletions(-) create mode 100644 src/test/scala/org/sunbird/cloud/storage/service/TestGcloudStorageServiceGetPutSignedURL.scala diff --git a/src/main/scala/org/sunbird/cloud/storage/service/GcloudStorageService.scala b/src/main/scala/org/sunbird/cloud/storage/service/GcloudStorageService.scala index 6e5a582..de4b6bc 100644 --- a/src/main/scala/org/sunbird/cloud/storage/service/GcloudStorageService.scala +++ b/src/main/scala/org/sunbird/cloud/storage/service/GcloudStorageService.scala @@ -97,22 +97,40 @@ class GcloudStorageService(config: StorageConfig) extends BaseStorageService { throw new StorageServiceException("Missing google credentials params.") } val properties = additionalParams.get; + // extract optional flag to indicate resumable/chunked upload, defaulting to false + val isChunkedUpload: Boolean = properties.get("chunked").map(_.toLowerCase).contains("true") // getting credentials val credentials = ServiceAccountCredentials.fromPkcs8(properties.get("clientId").get, properties.get("clientEmail").get, properties.get("privateKeyPkcs8").get, properties.get("privateKeyIds").get, new java.util.ArrayList[String]()) // creating storage options val storage = StorageOptions.newBuilder.setProjectId(properties.get("projectId").get).setCredentials(credentials).build.getService - // setting header as application/octet-stream (required by google) - val extensionHeaders = Map(HttpHeaders.CONTENT_TYPE -> contentType.getOrElse(MimeTypes.OCTET_STREAM)) // creating blob info val blobInfo = BlobInfo.newBuilder(BlobId.of(container, objectKey)).build // expiry time validation as TTL cannot be greater than 604800 // expiry time will be set to default value of 604800 if greater than 604800 val expiryTime = if(ttl.get > maxSignedurlTTL) maxSignedurlTTL else ttl.get //creating signed url - val url = storage.signUrl(blobInfo, expiryTime, TimeUnit.SECONDS, Storage.SignUrlOption.httpMethod(HttpMethod.PUT), -// Storage.SignUrlOption.withExtHeaders(extensionHeaders.asJava), - Storage.SignUrlOption.withV4Signature); + val url = if (isChunkedUpload) { + // Only for chunked uploads, use resumable POST and include required headers + val extensionHeaders = Map(HttpHeaders.CONTENT_TYPE -> contentType.getOrElse(MimeTypes.OCTET_STREAM), "x-goog-resumable" -> "start") + storage.signUrl( + blobInfo, + expiryTime, + TimeUnit.SECONDS, + Storage.SignUrlOption.httpMethod(HttpMethod.POST), + Storage.SignUrlOption.withV4Signature, + Storage.SignUrlOption.withExtHeaders(extensionHeaders.asJava) + ) + } else { + // Original behavior: simple PUT without extra headers + storage.signUrl( + blobInfo, + expiryTime, + TimeUnit.SECONDS, + Storage.SignUrlOption.httpMethod(HttpMethod.PUT), + Storage.SignUrlOption.withV4Signature + ) + } url.toString; } diff --git a/src/test/scala/org/sunbird/cloud/storage/service/TestGcloudStorageServiceGetPutSignedURL.scala b/src/test/scala/org/sunbird/cloud/storage/service/TestGcloudStorageServiceGetPutSignedURL.scala new file mode 100644 index 0000000..b8c66fa --- /dev/null +++ b/src/test/scala/org/sunbird/cloud/storage/service/TestGcloudStorageServiceGetPutSignedURL.scala @@ -0,0 +1,52 @@ +package org.sunbird.cloud.storage.service + +import org.scalatest.{FlatSpec, Matchers} +import org.sunbird.cloud.storage.conf.AppConf +import org.sunbird.cloud.storage.factory.{StorageConfig, StorageServiceFactory} +import scala.io.Source +import org.sunbird.cloud.storage.util.JSONUtils + +class TestGcloudStorageServiceGetPutSignedURL extends FlatSpec with Matchers { + + ignore should "return a valid V4 PUT signed URL for GCP" in { + + // 1) Compose service via factory + val gsService = StorageServiceFactory.getStorageService(StorageConfig("gcloud", AppConf.getStorageKey, AppConf.getStorageSecret)) + + val storageContainer = AppConf.getConfig("gcloud_storage_container") + // val objectKey = s"testUpload/test-put-signed-${System.currentTimeMillis()}.log" + val objectKey = s"testUpload/lesffichage.mp4" + + // 2) Build additionalParams from gcs-creds.json using JSONUtils + val credsPath = "src/test/resources/gcs-creds.json" + val credsJson = Source.fromFile(credsPath).getLines().mkString + val creds: Map[String, Any] = JSONUtils.deserialize[Map[String, Any]](credsJson) + + def str(key: String): String = creds.get(key).map(_.toString).getOrElse("") + + val privateKeyFixed = str("private_key").replace("\\n", "\n") + + val additionalParams: Map[String, String] = Map( + "clientId" -> str("client_id"), + "clientEmail" -> str("client_email"), + "privateKeyPkcs8" -> privateKeyFixed, + "privateKeyIds" -> str("private_key_id"), + "projectId" -> str("project_id"), + "chunked" -> "true" + ) + + // Call getPutSignedURL with write permission and a contentType + val ttlSeconds = Option(86400) + val contentType = Option("video/mp4") + val url = gsService.getPutSignedURL(storageContainer, objectKey, ttlSeconds, Option("w"), contentType, Option(additionalParams)) + println("Put Signed URL: " + url) + + // Basic validity assertions + url should include ("https://storage.googleapis.com/") + url should include (storageContainer) + url should include (objectKey) + url.toLowerCase should include ("x-goog-signature") + + gsService.closeContext() + } +} From 89de87e32883f41f773130c816033cf0cee69fcb Mon Sep 17 00:00:00 2001 From: Mahesh Kumar Gangula Date: Wed, 24 Sep 2025 16:15:45 +0530 Subject: [PATCH 09/47] feat: Support for generating PreSignedURL for read operation also. --- .../service/GcloudStorageService.scala | 71 +++++++++++-------- 1 file changed, 41 insertions(+), 30 deletions(-) diff --git a/src/main/scala/org/sunbird/cloud/storage/service/GcloudStorageService.scala b/src/main/scala/org/sunbird/cloud/storage/service/GcloudStorageService.scala index de4b6bc..013f822 100644 --- a/src/main/scala/org/sunbird/cloud/storage/service/GcloudStorageService.scala +++ b/src/main/scala/org/sunbird/cloud/storage/service/GcloudStorageService.scala @@ -93,43 +93,54 @@ class GcloudStorageService(config: StorageConfig) extends BaseStorageService { */ override def getPutSignedURL(container: String, objectKey: String, ttl: Option[Int], permission: Option[String] = Option("r"), contentType: Option[String] = Option("application/octet-stream"), additionalParams: Option[Map[String,String]] = None): String = { - if(additionalParams.get == None) { + if(additionalParams.isEmpty) { throw new StorageServiceException("Missing google credentials params.") } - val properties = additionalParams.get; - // extract optional flag to indicate resumable/chunked upload, defaulting to false - val isChunkedUpload: Boolean = properties.get("chunked").map(_.toLowerCase).contains("true") - // getting credentials + // expiry time validation as TTL cannot be greater than 604800 + // expiry time will be set to default value of 604800 if greater than 604800 + val properties = additionalParams.get + // prepare common credentials, storage and blob info once val credentials = ServiceAccountCredentials.fromPkcs8(properties.get("clientId").get, properties.get("clientEmail").get, properties.get("privateKeyPkcs8").get, - properties.get("privateKeyIds").get, new java.util.ArrayList[String]()) - // creating storage options + properties.get("privateKeyIds").get, new java.util.ArrayList[String]()) val storage = StorageOptions.newBuilder.setProjectId(properties.get("projectId").get).setCredentials(credentials).build.getService - // creating blob info val blobInfo = BlobInfo.newBuilder(BlobId.of(container, objectKey)).build - // expiry time validation as TTL cannot be greater than 604800 - // expiry time will be set to default value of 604800 if greater than 604800 val expiryTime = if(ttl.get > maxSignedurlTTL) maxSignedurlTTL else ttl.get //creating signed url - val url = if (isChunkedUpload) { - // Only for chunked uploads, use resumable POST and include required headers - val extensionHeaders = Map(HttpHeaders.CONTENT_TYPE -> contentType.getOrElse(MimeTypes.OCTET_STREAM), "x-goog-resumable" -> "start") - storage.signUrl( - blobInfo, - expiryTime, - TimeUnit.SECONDS, - Storage.SignUrlOption.httpMethod(HttpMethod.POST), - Storage.SignUrlOption.withV4Signature, - Storage.SignUrlOption.withExtHeaders(extensionHeaders.asJava) - ) - } else { - // Original behavior: simple PUT without extra headers - storage.signUrl( - blobInfo, - expiryTime, - TimeUnit.SECONDS, - Storage.SignUrlOption.httpMethod(HttpMethod.PUT), - Storage.SignUrlOption.withV4Signature - ) + val effectivePermission = permission.getOrElse("r").toLowerCase + val url = effectivePermission match { + case "w" => + // extract optional flag to indicate resumable/chunked upload, defaulting to false + val isChunkedUpload: Boolean = properties.get("chunked").map(_.toLowerCase).contains("true") + if (isChunkedUpload) { + // Only for chunked uploads, use resumable POST and include required headers + val extensionHeaders = Map(HttpHeaders.CONTENT_TYPE -> contentType.getOrElse(MimeTypes.OCTET_STREAM), "x-goog-resumable" -> "start") + storage.signUrl( + blobInfo, + expiryTime, + TimeUnit.SECONDS, + Storage.SignUrlOption.httpMethod(HttpMethod.POST), + Storage.SignUrlOption.withV4Signature, + Storage.SignUrlOption.withExtHeaders(extensionHeaders.asJava) + ) + } else { + // Original behavior: simple PUT without extra headers + storage.signUrl( + blobInfo, + expiryTime, + TimeUnit.SECONDS, + Storage.SignUrlOption.httpMethod(HttpMethod.PUT), + Storage.SignUrlOption.withV4Signature + ) + } + case _ => + // Default and for any invalid value: generate READ signed URL (GET) + storage.signUrl( + blobInfo, + expiryTime, + TimeUnit.SECONDS, + Storage.SignUrlOption.httpMethod(HttpMethod.GET), + Storage.SignUrlOption.withV4Signature + ) } url.toString; } From d65eebd99bfccb3f5b877a90e27a3157aa4269c6 Mon Sep 17 00:00:00 2001 From: Mahesh Kumar Gangula Date: Wed, 24 Sep 2025 16:21:53 +0530 Subject: [PATCH 10/47] feat: update the version of the project. --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 6c5af79..0c9935b 100644 --- a/pom.xml +++ b/pom.xml @@ -2,7 +2,7 @@ 4.0.0 org.sunbird cloud-store-sdk_2.12 - 1.4.7.2 + 1.4.7.3 jar Cloud Store SDK cloud-store-sdk provides client APIs to handle cloud store (AWS S3, Azure Blobstore, GCP, OCI). From 698291abcea913d30ce3b45602bbf5a2d29ae877 Mon Sep 17 00:00:00 2001 From: Chethan Date: Sun, 2 Nov 2025 21:28:25 +0530 Subject: [PATCH 11/47] feat: update project dependencies and Scala version to 2.13 --- pom.xml | 16 ++++++++-------- .../cloud/storage/BaseStorageService.scala | 2 +- .../storage/service/GcloudStorageService.scala | 2 +- .../service/TestAzureStorageService.scala | 4 +++- .../service/TestGcloudStorageService.scala | 5 +++-- ...TestGcloudStorageServiceGetPutSignedURL.scala | 5 +++-- .../service/TestOCIS3StorageService.scala | 5 +++-- .../storage/service/TestS3StorageService.scala | 6 +++--- 8 files changed, 25 insertions(+), 20 deletions(-) diff --git a/pom.xml b/pom.xml index 0c9935b..9b4286d 100644 --- a/pom.xml +++ b/pom.xml @@ -1,20 +1,20 @@ 4.0.0 org.sunbird - cloud-store-sdk_2.12 + cloud-store-sdk_2.13 1.4.7.3 jar Cloud Store SDK cloud-store-sdk provides client APIs to handle cloud store (AWS S3, Azure Blobstore, GCP, OCI). https://www.sunbird.org/ - 1.7 - 1.7 + 1.8 + 1.8 UTF-8 - 1.1.1 - 2.12 + 1.4.4 + 2.13 2.16.0 - 2.12.11 + 2.13.12 @@ -179,7 +179,7 @@ org.scalatest scalatest_${scala.maj.version} - 3.0.8 + 3.2.15 test @@ -206,7 +206,7 @@ net.alchim31.maven scala-maven-plugin - 3.2.2 + 4.4.0 diff --git a/src/main/scala/org/sunbird/cloud/storage/BaseStorageService.scala b/src/main/scala/org/sunbird/cloud/storage/BaseStorageService.scala index 934393d..12af232 100644 --- a/src/main/scala/org/sunbird/cloud/storage/BaseStorageService.scala +++ b/src/main/scala/org/sunbird/cloud/storage/BaseStorageService.scala @@ -9,7 +9,7 @@ import org.apache.tika.Tika import org.sunbird.cloud.storage.exception.StorageServiceException import org.sunbird.cloud.storage.util.{CommonUtil, JSONUtils} -import collection.JavaConverters._ +import scala.jdk.CollectionConverters._ import org.jclouds.blobstore.options.ListContainerOptions.Builder.{afterMarker, prefix, recursive} import org.sunbird.cloud.storage.Model.Blob import org.jclouds.blobstore.options.{CopyOptions, PutOptions} diff --git a/src/main/scala/org/sunbird/cloud/storage/service/GcloudStorageService.scala b/src/main/scala/org/sunbird/cloud/storage/service/GcloudStorageService.scala index 013f822..ffa23aa 100644 --- a/src/main/scala/org/sunbird/cloud/storage/service/GcloudStorageService.scala +++ b/src/main/scala/org/sunbird/cloud/storage/service/GcloudStorageService.scala @@ -14,7 +14,7 @@ import org.apache.tika.mime.MimeTypes import java.io.{File, FileOutputStream} import java.util.concurrent.TimeUnit -import scala.collection.JavaConverters._ +import scala.jdk.CollectionConverters._ class GcloudStorageService(config: StorageConfig) extends BaseStorageService { diff --git a/src/test/scala/org/sunbird/cloud/storage/service/TestAzureStorageService.scala b/src/test/scala/org/sunbird/cloud/storage/service/TestAzureStorageService.scala index 553aad1..4a72aaf 100644 --- a/src/test/scala/org/sunbird/cloud/storage/service/TestAzureStorageService.scala +++ b/src/test/scala/org/sunbird/cloud/storage/service/TestAzureStorageService.scala @@ -3,10 +3,12 @@ package org.sunbird.cloud.storage.service import org.sunbird.cloud.storage.factory.StorageConfig import org.sunbird.cloud.storage.factory.StorageServiceFactory import org.scalatest._ +import org.scalatest.flatspec.AnyFlatSpec +import org.scalatest.matchers.should.Matchers import org.sunbird.cloud.storage.conf.AppConf import org.sunbird.cloud.storage.exception.StorageServiceException -class TestAzureStorageService extends FlatSpec with Matchers { +class TestAzureStorageService extends AnyFlatSpec with Matchers { it should "test for azure storage" in { diff --git a/src/test/scala/org/sunbird/cloud/storage/service/TestGcloudStorageService.scala b/src/test/scala/org/sunbird/cloud/storage/service/TestGcloudStorageService.scala index 31c308a..a91c482 100644 --- a/src/test/scala/org/sunbird/cloud/storage/service/TestGcloudStorageService.scala +++ b/src/test/scala/org/sunbird/cloud/storage/service/TestGcloudStorageService.scala @@ -1,11 +1,12 @@ package org.sunbird.cloud.storage.service -import org.scalatest.{FlatSpec, Matchers} +import org.scalatest.flatspec.AnyFlatSpec +import org.scalatest.matchers.should.Matchers import org.sunbird.cloud.storage.conf.AppConf import org.sunbird.cloud.storage.exception.StorageServiceException import org.sunbird.cloud.storage.factory.{StorageConfig, StorageServiceFactory} -class TestGcloudStorageService extends FlatSpec with Matchers { +class TestGcloudStorageService extends AnyFlatSpec with Matchers { it should "test for gcloud storage" in { diff --git a/src/test/scala/org/sunbird/cloud/storage/service/TestGcloudStorageServiceGetPutSignedURL.scala b/src/test/scala/org/sunbird/cloud/storage/service/TestGcloudStorageServiceGetPutSignedURL.scala index b8c66fa..c2767c7 100644 --- a/src/test/scala/org/sunbird/cloud/storage/service/TestGcloudStorageServiceGetPutSignedURL.scala +++ b/src/test/scala/org/sunbird/cloud/storage/service/TestGcloudStorageServiceGetPutSignedURL.scala @@ -1,12 +1,13 @@ package org.sunbird.cloud.storage.service -import org.scalatest.{FlatSpec, Matchers} +import org.scalatest.flatspec.AnyFlatSpec +import org.scalatest.matchers.should.Matchers import org.sunbird.cloud.storage.conf.AppConf import org.sunbird.cloud.storage.factory.{StorageConfig, StorageServiceFactory} import scala.io.Source import org.sunbird.cloud.storage.util.JSONUtils -class TestGcloudStorageServiceGetPutSignedURL extends FlatSpec with Matchers { +class TestGcloudStorageServiceGetPutSignedURL extends AnyFlatSpec with Matchers { ignore should "return a valid V4 PUT signed URL for GCP" in { diff --git a/src/test/scala/org/sunbird/cloud/storage/service/TestOCIS3StorageService.scala b/src/test/scala/org/sunbird/cloud/storage/service/TestOCIS3StorageService.scala index cd36866..92581c1 100644 --- a/src/test/scala/org/sunbird/cloud/storage/service/TestOCIS3StorageService.scala +++ b/src/test/scala/org/sunbird/cloud/storage/service/TestOCIS3StorageService.scala @@ -1,11 +1,12 @@ package org.sunbird.cloud.storage.service -import org.scalatest.{FlatSpec, Matchers} +import org.scalatest.flatspec.AnyFlatSpec +import org.scalatest.matchers.should.Matchers import org.sunbird.cloud.storage.conf.AppConf import org.sunbird.cloud.storage.exception.StorageServiceException import org.sunbird.cloud.storage.factory.{StorageConfig, StorageServiceFactory} -class TestOCIS3StorageService extends FlatSpec with Matchers { +class TestOCIS3StorageService extends AnyFlatSpec with Matchers { it should "test for OCIS3 storage" in { diff --git a/src/test/scala/org/sunbird/cloud/storage/service/TestS3StorageService.scala b/src/test/scala/org/sunbird/cloud/storage/service/TestS3StorageService.scala index 96522c4..c8b1564 100644 --- a/src/test/scala/org/sunbird/cloud/storage/service/TestS3StorageService.scala +++ b/src/test/scala/org/sunbird/cloud/storage/service/TestS3StorageService.scala @@ -1,12 +1,12 @@ package org.sunbird.cloud.storage.service -import org.scalatest.FlatSpec -import org.scalatest._ +import org.scalatest.flatspec.AnyFlatSpec +import org.scalatest.matchers.should.Matchers import org.sunbird.cloud.storage.conf.AppConf import org.sunbird.cloud.storage.exception.StorageServiceException import org.sunbird.cloud.storage.factory.{StorageConfig, StorageServiceFactory} -class TestS3StorageService extends FlatSpec with Matchers { +class TestS3StorageService extends AnyFlatSpec with Matchers { it should "test for s3 storage" in { From 801311b6ba9480398c33ebb7a7438576c8ca5f9e Mon Sep 17 00:00:00 2001 From: Chethan Date: Mon, 3 Nov 2025 11:53:23 +0530 Subject: [PATCH 12/47] feat: update version to 1.4.8 in pom.xml --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 9b4286d..4de40cf 100644 --- a/pom.xml +++ b/pom.xml @@ -2,7 +2,7 @@ 4.0.0 org.sunbird cloud-store-sdk_2.13 - 1.4.7.3 + 1.4.8 jar Cloud Store SDK cloud-store-sdk provides client APIs to handle cloud store (AWS S3, Azure Blobstore, GCP, OCI). From 252ac78f925da6b21e35668d27ff44b311bbb978 Mon Sep 17 00:00:00 2001 From: Chethan Date: Fri, 14 Nov 2025 08:29:00 +0530 Subject: [PATCH 13/47] feat: update version to 1.4.8.1 and upgrade dependencies in pom.xml --- pom.xml | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/pom.xml b/pom.xml index 4de40cf..0d1ecdb 100644 --- a/pom.xml +++ b/pom.xml @@ -2,7 +2,7 @@ 4.0.0 org.sunbird cloud-store-sdk_2.13 - 1.4.8 + 1.4.8.1 jar Cloud Store SDK cloud-store-sdk provides client APIs to handle cloud store (AWS S3, Azure Blobstore, GCP, OCI). @@ -55,7 +55,7 @@ com.google.guava guava - 19.0 + 31.1-jre junit @@ -66,7 +66,7 @@ org.apache.jclouds jclouds-all - 2.2.1 + 2.5.0 com.jamesmurty.utils @@ -76,6 +76,10 @@ org.yaml snakeyaml + + com.google.gson + gson + @@ -93,6 +97,11 @@ java-xmlbuilder 1.2 + + com.google.code.gson + gson + 2.8.9 + com.microsoft.azure azure-storage From 6aba98707e6772ef47c5f0ea32081f1b3ad40f88 Mon Sep 17 00:00:00 2001 From: Mahesh Kumar Gangula <6985261+maheshkumargangula@users.noreply.github.com> Date: Tue, 13 Jan 2026 16:15:48 +0530 Subject: [PATCH 14/47] feat: AWS S3 APIs using IAM role and policies Deprecating Access Key usage and using Service Account or IAM roles. --- pom.xml | 8 ++- .../sunbird/cloud/storage/conf/AppConf.scala | 5 ++ .../factory/StorageServiceFactory.scala | 13 +++- .../storage/service/S3IAMStorageService.scala | 40 ++++++++++++ .../storage/util/IAMCredentialsSupplier.scala | 40 ++++++++++++ .../service/TestS3IAMStorageService.scala | 62 +++++++++++++++++++ 6 files changed, 164 insertions(+), 4 deletions(-) create mode 100644 src/main/scala/org/sunbird/cloud/storage/service/S3IAMStorageService.scala create mode 100644 src/main/scala/org/sunbird/cloud/storage/util/IAMCredentialsSupplier.scala create mode 100644 src/test/scala/org/sunbird/cloud/storage/service/TestS3IAMStorageService.scala diff --git a/pom.xml b/pom.xml index 0d1ecdb..2f2c6d8 100644 --- a/pom.xml +++ b/pom.xml @@ -2,7 +2,7 @@ 4.0.0 org.sunbird cloud-store-sdk_2.13 - 1.4.8.1 + 1.4.9 jar Cloud Store SDK cloud-store-sdk provides client APIs to handle cloud store (AWS S3, Azure Blobstore, GCP, OCI). @@ -206,6 +206,12 @@ tika-core 2.9.0 + + + software.amazon.awssdk + auth + 2.25.0 + src/main/scala diff --git a/src/main/scala/org/sunbird/cloud/storage/conf/AppConf.scala b/src/main/scala/org/sunbird/cloud/storage/conf/AppConf.scala index 7aa4394..ab09939 100644 --- a/src/main/scala/org/sunbird/cloud/storage/conf/AppConf.scala +++ b/src/main/scala/org/sunbird/cloud/storage/conf/AppConf.scala @@ -24,6 +24,11 @@ object AppConf { def getStorageSecret: String = getConfig("cloud_storage_secret") + def getAuthType: String = { + val authType = getConfig("cloud_storage_auth_type") + if (authType.nonEmpty) authType else "access_key" + } + def getRegion: Option[String] = { if (getStorageType.equals("oci")) Option(getConfig("cloud_storage_region")) diff --git a/src/main/scala/org/sunbird/cloud/storage/factory/StorageServiceFactory.scala b/src/main/scala/org/sunbird/cloud/storage/factory/StorageServiceFactory.scala index b0e18c0..776c7b2 100644 --- a/src/main/scala/org/sunbird/cloud/storage/factory/StorageServiceFactory.scala +++ b/src/main/scala/org/sunbird/cloud/storage/factory/StorageServiceFactory.scala @@ -2,12 +2,16 @@ package org.sunbird.cloud.storage.factory import org.sunbird.cloud.storage.BaseStorageService import org.sunbird.cloud.storage.exception.StorageServiceException -import org.sunbird.cloud.storage.service.{AzureStorageService, CephS3StorageService, GcloudStorageService, OCIS3StorageService, S3StorageService} +import org.sunbird.cloud.storage.service.{AzureStorageService, CephS3StorageService, GcloudStorageService, OCIS3StorageService, S3StorageService, S3IAMStorageService} -case class StorageConfig(`type`: String, storageKey: String, storageSecret: String, endPoint: Option[String] = None, region: Option[String] = Option("")) +case class StorageConfig(`type`: String, storageKey: String, storageSecret: String, endPoint: Option[String] = None, region: Option[String] = Option(""), authType: String = "access_key") object StorageServiceFactory { + private def isIAMAuth(authType: String): Boolean = { + Set("iam", "iam_role", "instance_profile").contains(authType.toLowerCase) + } + /** * Creates StorageService based on the given config * @param config @@ -16,7 +20,10 @@ object StorageServiceFactory { def getStorageService(config: StorageConfig): BaseStorageService = { config.`type`.toLowerCase() match { case "aws" => - new S3StorageService(config); + if (isIAMAuth(config.authType)) + new S3IAMStorageService(config) + else + new S3StorageService(config) case "azure" => new AzureStorageService(config); case "cephs3" => diff --git a/src/main/scala/org/sunbird/cloud/storage/service/S3IAMStorageService.scala b/src/main/scala/org/sunbird/cloud/storage/service/S3IAMStorageService.scala new file mode 100644 index 0000000..01c0f54 --- /dev/null +++ b/src/main/scala/org/sunbird/cloud/storage/service/S3IAMStorageService.scala @@ -0,0 +1,40 @@ +package org.sunbird.cloud.storage.service + +import org.jclouds.ContextBuilder +import org.jclouds.blobstore.BlobStoreContext +import org.sunbird.cloud.storage.BaseStorageService +import org.sunbird.cloud.storage.Model.Blob +import org.sunbird.cloud.storage.factory.StorageConfig +import org.sunbird.cloud.storage.util.IAMCredentialsSupplier +import java.util.Properties + +/** + * S3 Storage Service that uses IAM Role-based authentication. + * Uses AWS SDK v2 DefaultCredentialsProvider to resolve credentials from: + * - EC2 Instance Profile + * - ECS Task Role + * - EKS IRSA (IAM Roles for Service Accounts) + * - Environment variables + * - AWS credentials file + */ +class S3IAMStorageService(config: StorageConfig) extends BaseStorageService { + + // Region configuration (recommended for IAM auth) + private val awsRegion = config.region.filter(_.nonEmpty).getOrElse("us-east-1") + + // Configure region via JClouds properties + private val overrides = new Properties() + overrides.setProperty("jclouds.regions", awsRegion) + + // Use credentialsSupplier for dynamic IAM credential resolution + var context = ContextBuilder.newBuilder("aws-s3") + .credentialsSupplier(new IAMCredentialsSupplier()) + .overrides(overrides) + .buildView(classOf[BlobStoreContext]) + + var blobStore = context.getBlobStore + + override def getPaths(container: String, objects: List[Blob]): List[String] = { + objects.map(f => "s3n://" + container + "/" + f.key) + } +} diff --git a/src/main/scala/org/sunbird/cloud/storage/util/IAMCredentialsSupplier.scala b/src/main/scala/org/sunbird/cloud/storage/util/IAMCredentialsSupplier.scala new file mode 100644 index 0000000..0005033 --- /dev/null +++ b/src/main/scala/org/sunbird/cloud/storage/util/IAMCredentialsSupplier.scala @@ -0,0 +1,40 @@ +package org.sunbird.cloud.storage.util + +import software.amazon.awssdk.auth.credentials.{DefaultCredentialsProvider, AwsCredentials, AwsSessionCredentials} +import org.jclouds.domain.Credentials +import org.jclouds.aws.domain.SessionCredentials +import com.google.common.base.Supplier + +/** + * A Guava Supplier that bridges AWS SDK v2 credential resolution with JClouds. + * Uses DefaultCredentialsProvider which checks credentials in this order: + * 1. System properties (aws.accessKeyId, aws.secretAccessKey) + * 2. Environment variables (AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY) + * 3. Web Identity Token (EKS IRSA) + * 4. Credential profiles (~/.aws/credentials) + * 5. ECS Container credentials + * 6. EC2 Instance Profile (IMDS) + * + * For IAM roles (EC2, ECS, EKS), temporary credentials with session tokens are returned. + */ +class IAMCredentialsSupplier extends Supplier[Credentials] { + + private val awsProvider = DefaultCredentialsProvider.builder().build() + + override def get(): Credentials = { + val awsCreds: AwsCredentials = awsProvider.resolveCredentials() + + awsCreds match { + case sessionCreds: AwsSessionCredentials => + // IAM role credentials include a session token that must be passed to JClouds + SessionCredentials.builder() + .accessKeyId(sessionCreds.accessKeyId()) + .secretAccessKey(sessionCreds.secretAccessKey()) + .sessionToken(sessionCreds.sessionToken()) + .build() + case _ => + // Static credentials (from env vars, config file, etc.) + new Credentials(awsCreds.accessKeyId(), awsCreds.secretAccessKey()) + } + } +} diff --git a/src/test/scala/org/sunbird/cloud/storage/service/TestS3IAMStorageService.scala b/src/test/scala/org/sunbird/cloud/storage/service/TestS3IAMStorageService.scala new file mode 100644 index 0000000..d0d2ee5 --- /dev/null +++ b/src/test/scala/org/sunbird/cloud/storage/service/TestS3IAMStorageService.scala @@ -0,0 +1,62 @@ +package org.sunbird.cloud.storage.service + +import org.scalatest.flatspec.AnyFlatSpec +import org.scalatest.matchers.should.Matchers +import org.sunbird.cloud.storage.factory.{StorageConfig, StorageServiceFactory} + +/** + * Test S3 storage service with IAM role authentication. + * Run this test on an EC2 instance with IAM role attached. + * + * Prerequisites: + * 1. EC2 instance with IAM role that has S3 read permissions + * 2. Update 'bucket' variable with your actual S3 bucket name + * 3. Update 'region' in StorageConfig with your bucket's region + * + * Run: mvn test -Dtest=TestS3IAMStorageService + */ +class TestS3IAMStorageService extends AnyFlatSpec with Matchers { + + // TODO: Replace with your actual bucket name and region before running on EC2 + val bucket = "your-test-bucket" + val region = "us-east-1" + + ignore should "test S3 storage with IAM role authentication" in { + println("=== Starting IAM Role Authentication Test ===\n") + + // Create service with IAM role auth (no access keys needed) + val s3Service = StorageServiceFactory.getStorageService( + StorageConfig("aws", "", "", None, Option(region), "iam_role") + ) + + // Test 1: List objects in bucket + println("=== Test 1: listObjectKeys ===") + val keys = s3Service.listObjectKeys(bucket, "") + println(s"Found ${keys.size} objects in bucket '$bucket'") + if (keys.nonEmpty) { + println("First 5 objects:") + keys.take(5).foreach(k => println(s" - $k")) + } + + // Test 2: Get object metadata (if objects exist) + if (keys.nonEmpty) { + println("\n=== Test 2: getObject ===") + val blob = s3Service.getObject(bucket, keys.head) + println(s"Object key: ${blob.key}") + println(s"Content length: ${blob.contentLength} bytes") + println(s"Last modified: ${blob.lastModified}") + } else { + println("\n=== Test 2: getObject (skipped - no objects in bucket) ===") + } + + // Test 3: Search objects with prefix + println("\n=== Test 3: searchObjectkeys ===") + val searchResults = s3Service.searchObjectkeys(bucket, "") + println(s"Search returned ${searchResults.size} objects") + + // Cleanup + s3Service.closeContext() + + println("\n=== IAM Role Authentication Test COMPLETED ===") + } +} From bdefdafb8f736a5dab650d689d6849b7d30393ce Mon Sep 17 00:00:00 2001 From: Mahesh Kumar Gangula <6985261+maheshkumargangula@users.noreply.github.com> Date: Tue, 13 Jan 2026 17:13:36 +0530 Subject: [PATCH 15/47] fix: the backward compatibility for new attribute (authType) --- .../cloud/storage/factory/StorageServiceFactory.scala | 6 +++--- .../cloud/storage/service/TestS3IAMStorageService.scala | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/main/scala/org/sunbird/cloud/storage/factory/StorageServiceFactory.scala b/src/main/scala/org/sunbird/cloud/storage/factory/StorageServiceFactory.scala index 776c7b2..4b2fd21 100644 --- a/src/main/scala/org/sunbird/cloud/storage/factory/StorageServiceFactory.scala +++ b/src/main/scala/org/sunbird/cloud/storage/factory/StorageServiceFactory.scala @@ -4,12 +4,12 @@ import org.sunbird.cloud.storage.BaseStorageService import org.sunbird.cloud.storage.exception.StorageServiceException import org.sunbird.cloud.storage.service.{AzureStorageService, CephS3StorageService, GcloudStorageService, OCIS3StorageService, S3StorageService, S3IAMStorageService} -case class StorageConfig(`type`: String, storageKey: String, storageSecret: String, endPoint: Option[String] = None, region: Option[String] = Option(""), authType: String = "access_key") +case class StorageConfig(`type`: String, storageKey: String, storageSecret: String, endPoint: Option[String] = None, region: Option[String] = Option(""), authType: Option[String] = Option("access_key")) object StorageServiceFactory { - private def isIAMAuth(authType: String): Boolean = { - Set("iam", "iam_role", "instance_profile").contains(authType.toLowerCase) + private def isIAMAuth(authType: Option[String]): Boolean = { + authType.exists(at => Set("iam", "iam_role", "instance_profile").contains(at.toLowerCase)) } /** diff --git a/src/test/scala/org/sunbird/cloud/storage/service/TestS3IAMStorageService.scala b/src/test/scala/org/sunbird/cloud/storage/service/TestS3IAMStorageService.scala index d0d2ee5..d794a40 100644 --- a/src/test/scala/org/sunbird/cloud/storage/service/TestS3IAMStorageService.scala +++ b/src/test/scala/org/sunbird/cloud/storage/service/TestS3IAMStorageService.scala @@ -26,7 +26,7 @@ class TestS3IAMStorageService extends AnyFlatSpec with Matchers { // Create service with IAM role auth (no access keys needed) val s3Service = StorageServiceFactory.getStorageService( - StorageConfig("aws", "", "", None, Option(region), "iam_role") + StorageConfig("aws", "", "", None, Option(region), Option("iam_role")) ) // Test 1: List objects in bucket From b88e18a94e539dfd2b8907db62d2d6dbd8d7671c Mon Sep 17 00:00:00 2001 From: Shashank Date: Thu, 15 Jan 2026 23:19:02 +0530 Subject: [PATCH 16/47] maven-release automation --- .github/workflows/maven-release.yml | 64 +++++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) create mode 100644 .github/workflows/maven-release.yml diff --git a/.github/workflows/maven-release.yml b/.github/workflows/maven-release.yml new file mode 100644 index 0000000..bbeeb12 --- /dev/null +++ b/.github/workflows/maven-release.yml @@ -0,0 +1,64 @@ +name: Publish to Maven Central + +on: + workflow_dispatch: + release: + types: [created] + +jobs: + publish: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up JDK 11 + uses: actions/setup-java@v4 + with: + java-version: '11' + distribution: 'temurin' + cache: 'maven' + + - name: Import GPG key + env: + GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }} + GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }} + run: | + mkdir -p ~/.gnupg + chmod 700 ~/.gnupg + + cat > ~/.gnupg/gpg.conf < ~/.gnupg/gpg-agent.conf < Date: Thu, 15 Jan 2026 23:26:50 +0530 Subject: [PATCH 17/47] updated maven gpg plugin version --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 2f2c6d8..e5d77d2 100644 --- a/pom.xml +++ b/pom.xml @@ -283,7 +283,7 @@ org.apache.maven.plugins maven-gpg-plugin - 1.5 + 3.2.8 sign-artifacts From 1d793d7469484b0478fd750252fd91b829b8b691 Mon Sep 17 00:00:00 2001 From: Shashank Date: Fri, 16 Jan 2026 00:10:28 +0530 Subject: [PATCH 18/47] github action for jar automation --- .github/workflows/maven-release.yml | 28 ++++++++-------------------- pom.xml | 4 ++-- 2 files changed, 10 insertions(+), 22 deletions(-) diff --git a/.github/workflows/maven-release.yml b/.github/workflows/maven-release.yml index bbeeb12..e2b8e12 100644 --- a/.github/workflows/maven-release.yml +++ b/.github/workflows/maven-release.yml @@ -1,7 +1,9 @@ -name: Publish to Maven Central +name: Publish to Maven Central (Simplified) on: - workflow_dispatch: + push: + branches: + - '**' release: types: [created] @@ -25,25 +27,11 @@ jobs: GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }} GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }} run: | - mkdir -p ~/.gnupg - chmod 700 ~/.gnupg - - cat > ~/.gnupg/gpg.conf < ~/.gnupg/gpg-agent.conf <4.0.0 org.sunbird cloud-store-sdk_2.13 - 1.4.9 + 1.4.9.1 jar Cloud Store SDK cloud-store-sdk provides client APIs to handle cloud store (AWS S3, Azure Blobstore, GCP, OCI). @@ -283,7 +283,7 @@ org.apache.maven.plugins maven-gpg-plugin - 3.2.8 + 1.5 sign-artifacts From b234fa5617710949a9ffd29a0d711b6ef55b125c Mon Sep 17 00:00:00 2001 From: Shashank Date: Fri, 16 Jan 2026 10:16:08 +0530 Subject: [PATCH 19/47] reverted the sdk version --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 495d593..2f2c6d8 100644 --- a/pom.xml +++ b/pom.xml @@ -2,7 +2,7 @@ 4.0.0 org.sunbird cloud-store-sdk_2.13 - 1.4.9.1 + 1.4.9 jar Cloud Store SDK cloud-store-sdk provides client APIs to handle cloud store (AWS S3, Azure Blobstore, GCP, OCI). From aaa044b830d31c056abf79caf845da0ab0fa66ad Mon Sep 17 00:00:00 2001 From: Shashank Date: Fri, 16 Jan 2026 11:01:19 +0530 Subject: [PATCH 20/47] updated action name --- .github/workflows/maven-release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/maven-release.yml b/.github/workflows/maven-release.yml index e2b8e12..d26a007 100644 --- a/.github/workflows/maven-release.yml +++ b/.github/workflows/maven-release.yml @@ -1,4 +1,4 @@ -name: Publish to Maven Central (Simplified) +name: Publish to Maven Central on: push: From 8c9d9a04f6f51166655814d7f0f193b317e66203 Mon Sep 17 00:00:00 2001 From: Shashank Date: Fri, 16 Jan 2026 13:31:00 +0530 Subject: [PATCH 21/47] copilot review changes --- .github/workflows/maven-release.yml | 10 +++++----- pom.xml | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/maven-release.yml b/.github/workflows/maven-release.yml index d26a007..71b2c9f 100644 --- a/.github/workflows/maven-release.yml +++ b/.github/workflows/maven-release.yml @@ -28,11 +28,11 @@ jobs: GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }} run: | # Import the GPG key - echo "$GPG_PRIVATE_KEY" | base64 -d | gpg --batch --passphrase "$GPG_PASSPHRASE" --import - - # Get the key ID and set it as default - KEY_ID=$(gpg --list-secret-keys --keyid-format LONG | grep sec | head -1 | awk '{print $2}' | cut -d'/' -f2) - + if ! echo "$GPG_PRIVATE_KEY" | base64 -d | gpg --batch --passphrase "$GPG_PASSPHRASE" --import; then + echo "Failed to import GPG private key. Please verify that GPG_PRIVATE_KEY and GPG_PASSPHRASE are correctly configured." >&2 + exit 1 + fi + - name: Configure Maven settings uses: s4u/maven-settings-action@v3.0.0 with: diff --git a/pom.xml b/pom.xml index 2f2c6d8..cc03596 100644 --- a/pom.xml +++ b/pom.xml @@ -2,7 +2,7 @@ 4.0.0 org.sunbird cloud-store-sdk_2.13 - 1.4.9 + 1.4.9.2 jar Cloud Store SDK cloud-store-sdk provides client APIs to handle cloud store (AWS S3, Azure Blobstore, GCP, OCI). From 73b06ceeec2ec972345e43e2ed8900106a7f479f Mon Sep 17 00:00:00 2001 From: Shashank Date: Fri, 16 Jan 2026 13:32:53 +0530 Subject: [PATCH 22/47] reveted the version --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index cc03596..e218359 100644 --- a/pom.xml +++ b/pom.xml @@ -2,7 +2,7 @@ 4.0.0 org.sunbird cloud-store-sdk_2.13 - 1.4.9.2 + 1.4.9. jar Cloud Store SDK cloud-store-sdk provides client APIs to handle cloud store (AWS S3, Azure Blobstore, GCP, OCI). From 0e0ce16d985c1e88e540fd11c32a8c60dbbc0ab7 Mon Sep 17 00:00:00 2001 From: Shashank Date: Fri, 16 Jan 2026 14:29:51 +0530 Subject: [PATCH 23/47] reverted the changes --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index e218359..b2748c6 100644 --- a/pom.xml +++ b/pom.xml @@ -2,7 +2,7 @@ 4.0.0 org.sunbird cloud-store-sdk_2.13 - 1.4.9. + 1.4.9 jar Cloud Store SDK cloud-store-sdk provides client APIs to handle cloud store (AWS S3, Azure Blobstore, GCP, OCI). @@ -312,4 +312,4 @@ - + \ No newline at end of file From 07d528ba62d43a89fae8cdc7a67ce18334c8af56 Mon Sep 17 00:00:00 2001 From: Mahesh Kumar Gangula <6985261+maheshkumargangula@users.noreply.github.com> Date: Tue, 20 Jan 2026 17:20:47 +0530 Subject: [PATCH 24/47] feat: updates to use scala 2.12 and 2.13. --- pom.xml | 37 ++++++++++++++++++++++++++++++++++--- 1 file changed, 34 insertions(+), 3 deletions(-) diff --git a/pom.xml b/pom.xml index b2748c6..0847781 100644 --- a/pom.xml +++ b/pom.xml @@ -1,7 +1,7 @@ 4.0.0 org.sunbird - cloud-store-sdk_2.13 + cloud-store-sdk_${scala.maj.version} 1.4.9 jar Cloud Store SDK @@ -12,9 +12,7 @@ 1.8 UTF-8 1.4.4 - 2.13 2.16.0 - 2.13.12 @@ -212,6 +210,18 @@ auth 2.25.0 + + + software.amazon.awssdk + sts + 2.25.0 + + + + org.scala-lang.modules + scala-collection-compat_${scala.maj.version} + 2.11.0 + src/main/scala @@ -312,4 +322,25 @@ + + + + scala-2.13 + + true + + + 2.13 + 2.13.12 + + + + + scala-2.12 + + 2.12 + 2.12.18 + + + \ No newline at end of file From d44bc467c87885693624eb649e2000fcf4a6df36 Mon Sep 17 00:00:00 2001 From: aimansharief Date: Thu, 22 Jan 2026 10:23:50 +0530 Subject: [PATCH 25/47] feat: Added the flatten-maven-plugin to ensure all variables are resolved before deployment --- .gitignore | 1 + pom.xml | 28 ++++++++++++++++++++++++++-- 2 files changed, 27 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index 67858ce..df45414 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,4 @@ src/test/resources/test* .cache* .DS_Store *.iml +.flattened-pom.xml diff --git a/pom.xml b/pom.xml index 0847781..ad226a2 100644 --- a/pom.xml +++ b/pom.xml @@ -2,7 +2,7 @@ 4.0.0 org.sunbird cloud-store-sdk_${scala.maj.version} - 1.4.9 + 1.4.10 jar Cloud Store SDK cloud-store-sdk provides client APIs to handle cloud store (AWS S3, Azure Blobstore, GCP, OCI). @@ -320,6 +320,30 @@ central + + org.codehaus.mojo + flatten-maven-plugin + 1.5.0 + + ossrh + + + + flatten + process-resources + + flatten + + + + flatten.clean + clean + + clean + + + + @@ -343,4 +367,4 @@ - \ No newline at end of file + From ce81bd6867cf62030ba22c29ef0a26845a578874 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 19 Feb 2026 10:18:46 +0000 Subject: [PATCH 26/47] feat: Refactor to multi-module Java 11 SDK with CSP-specific implementations Replace the single Scala/jclouds-based module with a multi-module pure Java 11 Maven project. Each CSP (AWS, Azure, GCP, OCI) gets its own module using the official cloud SDK, with OIDC-based auth as the primary approach and Access Key as a configurable fallback. Modules: - cloud-storage-sdk-api: Interface, models, factory (ServiceLoader), abstract base - cloud-storage-sdk-aws: AWS S3 + Ceph S3 support (AWS SDK v2) - cloud-storage-sdk-azure: Azure Blob Storage (azure-storage-blob + azure-identity) - cloud-storage-sdk-gcp: Google Cloud Storage (google-cloud-storage) - cloud-storage-sdk-oci: OCI Object Storage (oci-java-sdk-objectstorage) Key changes: - Drop jclouds, Scala, and Joda-Time dependencies entirely - Pure Java 11 with java.time for date operations - ServiceLoader-based CSP discovery via StorageServiceProvider SPI - StorageConfig uses Builder pattern with AuthType and StorageType enums - AbstractStorageService provides shared orchestration (retry, search, copy, etc.) - IStorageService extends AutoCloseable for try-with-resources support https://claude.ai/code/session_01KrRXhCQDoUCac99315Fnh6 --- .circleci/config.yml | 15 +- .github/workflows/maven-release.yml | 16 +- .gitignore | 1 + PLAN.md | 546 ++++++++++++++++ README.md | 95 ++- cloud-storage-sdk-api/pom.xml | 36 ++ .../cloud/storage/AbstractStorageService.java | 425 +++++++++++++ .../cloud/storage/IStorageService.java | 302 +++++++++ .../sunbird/cloud/storage/StorageConfig.java | 106 ++++ .../cloud/storage/StorageServiceFactory.java | 39 ++ .../cloud/storage/StorageServiceProvider.java | 25 + .../exception/StorageServiceException.java | 15 + .../org/sunbird/cloud/storage/model/Blob.java | 56 ++ .../cloud/storage/model/DeleteTarget.java | 36 ++ .../cloud/storage/util/DateRangeUtil.java | 56 ++ .../sunbird/cloud/storage/util/FileUtil.java | 102 +++ cloud-storage-sdk-aws/pom.xml | 40 ++ .../service/aws/AwsStorageService.java | 372 +++++++++++ .../aws/AwsStorageServiceProvider.java | 22 + ...nbird.cloud.storage.StorageServiceProvider | 1 + cloud-storage-sdk-azure/pom.xml | 40 ++ .../service/azure/AzureStorageService.java | 289 +++++++++ .../azure/AzureStorageServiceProvider.java | 21 + ...nbird.cloud.storage.StorageServiceProvider | 1 + cloud-storage-sdk-gcp/pom.xml | 36 ++ .../service/gcp/GcpStorageService.java | 321 ++++++++++ .../gcp/GcpStorageServiceProvider.java | 21 + ...nbird.cloud.storage.StorageServiceProvider | 1 + cloud-storage-sdk-oci/pom.xml | 41 ++ .../service/oci/OciStorageService.java | 385 ++++++++++++ .../oci/OciStorageServiceProvider.java | 21 + ...nbird.cloud.storage.StorageServiceProvider | 1 + pom.xml | 586 +++++++----------- .../cloud/storage/BaseStorageService.scala | 331 ---------- .../cloud/storage/IStorageService.scala | 213 ------- .../org/sunbird/cloud/storage/Model.scala | 8 - .../sunbird/cloud/storage/conf/AppConf.scala | 43 -- .../sunbird/cloud/storage/conf/package.scala | 7 - .../exception/StorageServiceException.scala | 5 - .../cloud/storage/exception/package.scala | 7 - .../factory/StorageServiceFactory.scala | 39 -- .../cloud/storage/factory/package.scala | 7 - .../org/sunbird/cloud/storage/package.scala | 8 - .../storage/service/AzureStorageService.scala | 17 - .../service/CephS3StorageService.scala | 19 - .../service/GcloudStorageService.scala | 334 ---------- .../storage/service/OCIS3StorageService.scala | 59 -- .../storage/service/S3IAMStorageService.scala | 40 -- .../storage/service/S3StorageService.scala | 17 - .../cloud/storage/service/package.scala | 8 - .../cloud/storage/util/CommonUtil.scala | 98 --- .../storage/util/IAMCredentialsSupplier.scala | 40 -- .../cloud/storage/util/JSONUtils.scala | 48 -- .../sunbird/cloud/storage/util/package.scala | 7 - src/test/resources/1234/part-00000 | 286 --------- src/test/resources/1234/part-00001 | 277 --------- src/test/resources/application.conf | 21 - src/test/resources/test-data.log | 18 - src/test/resources/test-extract.zip | Bin 1444 -> 0 bytes .../service/TestAzureStorageService.scala | 72 --- .../service/TestGcloudStorageService.scala | 70 --- ...tGcloudStorageServiceGetPutSignedURL.scala | 53 -- .../service/TestOCIS3StorageService.scala | 68 -- .../service/TestS3IAMStorageService.scala | 62 -- .../service/TestS3StorageService.scala | 66 -- 65 files changed, 3708 insertions(+), 2710 deletions(-) create mode 100644 PLAN.md create mode 100644 cloud-storage-sdk-api/pom.xml create mode 100644 cloud-storage-sdk-api/src/main/java/org/sunbird/cloud/storage/AbstractStorageService.java create mode 100644 cloud-storage-sdk-api/src/main/java/org/sunbird/cloud/storage/IStorageService.java create mode 100644 cloud-storage-sdk-api/src/main/java/org/sunbird/cloud/storage/StorageConfig.java create mode 100644 cloud-storage-sdk-api/src/main/java/org/sunbird/cloud/storage/StorageServiceFactory.java create mode 100644 cloud-storage-sdk-api/src/main/java/org/sunbird/cloud/storage/StorageServiceProvider.java create mode 100644 cloud-storage-sdk-api/src/main/java/org/sunbird/cloud/storage/exception/StorageServiceException.java create mode 100644 cloud-storage-sdk-api/src/main/java/org/sunbird/cloud/storage/model/Blob.java create mode 100644 cloud-storage-sdk-api/src/main/java/org/sunbird/cloud/storage/model/DeleteTarget.java create mode 100644 cloud-storage-sdk-api/src/main/java/org/sunbird/cloud/storage/util/DateRangeUtil.java create mode 100644 cloud-storage-sdk-api/src/main/java/org/sunbird/cloud/storage/util/FileUtil.java create mode 100644 cloud-storage-sdk-aws/pom.xml create mode 100644 cloud-storage-sdk-aws/src/main/java/org/sunbird/cloud/storage/service/aws/AwsStorageService.java create mode 100644 cloud-storage-sdk-aws/src/main/java/org/sunbird/cloud/storage/service/aws/AwsStorageServiceProvider.java create mode 100644 cloud-storage-sdk-aws/src/main/resources/META-INF/services/org.sunbird.cloud.storage.StorageServiceProvider create mode 100644 cloud-storage-sdk-azure/pom.xml create mode 100644 cloud-storage-sdk-azure/src/main/java/org/sunbird/cloud/storage/service/azure/AzureStorageService.java create mode 100644 cloud-storage-sdk-azure/src/main/java/org/sunbird/cloud/storage/service/azure/AzureStorageServiceProvider.java create mode 100644 cloud-storage-sdk-azure/src/main/resources/META-INF/services/org.sunbird.cloud.storage.StorageServiceProvider create mode 100644 cloud-storage-sdk-gcp/pom.xml create mode 100644 cloud-storage-sdk-gcp/src/main/java/org/sunbird/cloud/storage/service/gcp/GcpStorageService.java create mode 100644 cloud-storage-sdk-gcp/src/main/java/org/sunbird/cloud/storage/service/gcp/GcpStorageServiceProvider.java create mode 100644 cloud-storage-sdk-gcp/src/main/resources/META-INF/services/org.sunbird.cloud.storage.StorageServiceProvider create mode 100644 cloud-storage-sdk-oci/pom.xml create mode 100644 cloud-storage-sdk-oci/src/main/java/org/sunbird/cloud/storage/service/oci/OciStorageService.java create mode 100644 cloud-storage-sdk-oci/src/main/java/org/sunbird/cloud/storage/service/oci/OciStorageServiceProvider.java create mode 100644 cloud-storage-sdk-oci/src/main/resources/META-INF/services/org.sunbird.cloud.storage.StorageServiceProvider delete mode 100644 src/main/scala/org/sunbird/cloud/storage/BaseStorageService.scala delete mode 100644 src/main/scala/org/sunbird/cloud/storage/IStorageService.scala delete mode 100644 src/main/scala/org/sunbird/cloud/storage/Model.scala delete mode 100644 src/main/scala/org/sunbird/cloud/storage/conf/AppConf.scala delete mode 100644 src/main/scala/org/sunbird/cloud/storage/conf/package.scala delete mode 100644 src/main/scala/org/sunbird/cloud/storage/exception/StorageServiceException.scala delete mode 100644 src/main/scala/org/sunbird/cloud/storage/exception/package.scala delete mode 100644 src/main/scala/org/sunbird/cloud/storage/factory/StorageServiceFactory.scala delete mode 100644 src/main/scala/org/sunbird/cloud/storage/factory/package.scala delete mode 100644 src/main/scala/org/sunbird/cloud/storage/package.scala delete mode 100644 src/main/scala/org/sunbird/cloud/storage/service/AzureStorageService.scala delete mode 100644 src/main/scala/org/sunbird/cloud/storage/service/CephS3StorageService.scala delete mode 100644 src/main/scala/org/sunbird/cloud/storage/service/GcloudStorageService.scala delete mode 100644 src/main/scala/org/sunbird/cloud/storage/service/OCIS3StorageService.scala delete mode 100644 src/main/scala/org/sunbird/cloud/storage/service/S3IAMStorageService.scala delete mode 100644 src/main/scala/org/sunbird/cloud/storage/service/S3StorageService.scala delete mode 100644 src/main/scala/org/sunbird/cloud/storage/service/package.scala delete mode 100644 src/main/scala/org/sunbird/cloud/storage/util/CommonUtil.scala delete mode 100644 src/main/scala/org/sunbird/cloud/storage/util/IAMCredentialsSupplier.scala delete mode 100644 src/main/scala/org/sunbird/cloud/storage/util/JSONUtils.scala delete mode 100644 src/main/scala/org/sunbird/cloud/storage/util/package.scala delete mode 100644 src/test/resources/1234/part-00000 delete mode 100644 src/test/resources/1234/part-00001 delete mode 100644 src/test/resources/application.conf delete mode 100644 src/test/resources/test-data.log delete mode 100644 src/test/resources/test-extract.zip delete mode 100644 src/test/scala/org/sunbird/cloud/storage/service/TestAzureStorageService.scala delete mode 100644 src/test/scala/org/sunbird/cloud/storage/service/TestGcloudStorageService.scala delete mode 100644 src/test/scala/org/sunbird/cloud/storage/service/TestGcloudStorageServiceGetPutSignedURL.scala delete mode 100644 src/test/scala/org/sunbird/cloud/storage/service/TestOCIS3StorageService.scala delete mode 100644 src/test/scala/org/sunbird/cloud/storage/service/TestS3IAMStorageService.scala delete mode 100644 src/test/scala/org/sunbird/cloud/storage/service/TestS3StorageService.scala diff --git a/.circleci/config.yml b/.circleci/config.yml index 0f54860..e4929a1 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -2,21 +2,30 @@ version: 2.1 jobs: cloud-storage-build: machine: - image: ubuntu-2004:202201-02 + image: ubuntu-2204:current steps: - checkout - restore_cache: key: cloud-storage-dependency-cache-{{ checksum "pom.xml" }} + - run: + name: Set up JDK 11 + command: | + sudo apt-get update && sudo apt-get install -y openjdk-11-jdk + export JAVA_HOME=/usr/lib/jvm/java-11-openjdk-amd64 + export PATH=$JAVA_HOME/bin:$PATH - run: name: Build - command: mvn scoverage:report + command: mvn clean verify -DskipTests - save_cache: key: cloud-storage-dependency-cache-{{ checksum "pom.xml" }} paths: ~/.m2 + - run: + name: Test + command: mvn test - run: name: sonar command: | - mvn -X sonar:sonar -Dsonar.projectKey=project-sunbird_sunbird-cloud-storage-sdk -Dsonar.host.url=https://sonarcloud.io -Dsonar.organization=project-sunbird + mvn -X sonar:sonar -Dsonar.projectKey=project-sunbird_sunbird-cloud-storage-sdk -Dsonar.host.url=https://sonarcloud.io -Dsonar.organization=project-sunbird workflows: version: 2.1 diff --git a/.github/workflows/maven-release.yml b/.github/workflows/maven-release.yml index 71b2c9f..afb54b2 100644 --- a/.github/workflows/maven-release.yml +++ b/.github/workflows/maven-release.yml @@ -10,26 +10,25 @@ on: jobs: publish: runs-on: ubuntu-latest - + steps: - name: Checkout code uses: actions/checkout@v4 - + - name: Set up JDK 11 uses: actions/setup-java@v4 with: java-version: '11' distribution: 'temurin' cache: 'maven' - + - name: Import GPG key env: GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }} GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }} run: | - # Import the GPG key if ! echo "$GPG_PRIVATE_KEY" | base64 -d | gpg --batch --passphrase "$GPG_PASSPHRASE" --import; then - echo "Failed to import GPG private key. Please verify that GPG_PRIVATE_KEY and GPG_PASSPHRASE are correctly configured." >&2 + echo "Failed to import GPG private key." >&2 exit 1 fi @@ -47,6 +46,9 @@ jobs: "gpg.executable": "gpg", "gpg.passphrase": "${{ secrets.GPG_PASSPHRASE }}" }] - + + - name: Build all modules + run: mvn clean verify -DskipTests + - name: Deploy to Maven Central - run: mvn deploy -DskipTests -Dskip.scalatest=true \ No newline at end of file + run: mvn deploy -DskipTests diff --git a/.gitignore b/.gitignore index df45414..886a23d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ /target/ +**/target/ .classpath .project .settings diff --git a/PLAN.md b/PLAN.md new file mode 100644 index 0000000..ada3275 --- /dev/null +++ b/PLAN.md @@ -0,0 +1,546 @@ +# Implementation Plan: Multi-Module Java 11 Cloud Storage SDK + +## Overview + +Refactor the Sunbird Cloud Storage SDK from a single Scala/jclouds-based module into a **multi-module pure Java 11 Maven project** with CSP-specific implementations using official cloud SDKs. Replaces all existing Scala source code. + +### Key Decisions +- **Ceph S3**: Folded into the AWS module via configurable endpoint override (no separate module) +- **OCI**: Uses official OCI Java SDK (`oci-java-sdk-objectstorage`) for native API and Instance Principal auth +- **Existing code**: Replaced entirely — clean break at version 2.0.0 + +--- + +## 1. Maven Module Structure + +### Directory Layout +``` +sunbird-cloud-storage-sdk/ +├── pom.xml (parent POM, packaging=pom) +├── cloud-storage-sdk-api/ +│ ├── pom.xml +│ └── src/main/java/org/sunbird/cloud/storage/ +│ ├── IStorageService.java (main interface) +│ ├── StorageConfig.java (config with Builder pattern) +│ ├── StorageServiceFactory.java (ServiceLoader-based factory) +│ ├── StorageServiceProvider.java (SPI interface) +│ ├── AbstractStorageService.java (shared orchestration logic) +│ ├── model/ +│ │ ├── Blob.java (data model) +│ │ └── DeleteTarget.java (replaces Scala tuple) +│ ├── exception/ +│ │ └── StorageServiceException.java (runtime exception) +│ └── util/ +│ ├── DateRangeUtil.java (java.time replacement for Joda-Time) +│ └── FileUtil.java (file copy, zip extraction) +├── cloud-storage-sdk-aws/ +│ ├── pom.xml +│ └── src/ +│ ├── main/java/org/sunbird/cloud/storage/service/aws/ +│ │ ├── AwsStorageService.java (AWS S3 + Ceph S3 implementation) +│ │ └── AwsStorageServiceProvider.java (SPI registration) +│ └── main/resources/META-INF/services/ +│ └── org.sunbird.cloud.storage.StorageServiceProvider +├── cloud-storage-sdk-azure/ +│ ├── pom.xml +│ └── src/ +│ ├── main/java/org/sunbird/cloud/storage/service/azure/ +│ │ ├── AzureStorageService.java +│ │ └── AzureStorageServiceProvider.java +│ └── main/resources/META-INF/services/ +│ └── org.sunbird.cloud.storage.StorageServiceProvider +├── cloud-storage-sdk-gcp/ +│ ├── pom.xml +│ └── src/ +│ ├── main/java/org/sunbird/cloud/storage/service/gcp/ +│ │ ├── GcpStorageService.java +│ │ └── GcpStorageServiceProvider.java +│ └── main/resources/META-INF/services/ +│ └── org.sunbird.cloud.storage.StorageServiceProvider +└── cloud-storage-sdk-oci/ + ├── pom.xml + └── src/ + ├── main/java/org/sunbird/cloud/storage/service/oci/ + │ ├── OciStorageService.java + │ └── OciStorageServiceProvider.java + └── main/resources/META-INF/services/ + └── org.sunbird.cloud.storage.StorageServiceProvider +``` + +### Artifact Summary + +| Module | Artifact ID | Key Dependencies | +|--------|-------------|------------------| +| Parent | `cloud-storage-sdk-parent` | — (pom packaging) | +| API | `cloud-storage-sdk-api` | SLF4J, Tika Core, Jackson Databind | +| AWS | `cloud-storage-sdk-aws` | API module + AWS SDK v2 (s3, sts) | +| Azure | `cloud-storage-sdk-azure` | API module + azure-storage-blob, azure-identity | +| GCP | `cloud-storage-sdk-gcp` | API module + google-cloud-storage | +| OCI | `cloud-storage-sdk-oci` | API module + oci-java-sdk-objectstorage | + +**Group ID**: `org.sunbird` (unchanged) +**Version**: `2.0.0` (major bump — breaking change from Scala to Java) + +--- + +## 2. Parent POM Configuration + +```xml + + 11 + 11 + 11 + UTF-8 + + + 2.25.0 + 12.25.0 + 1.11.0 + 2.30.0 + 3.30.0 + 2.9.0 + 2.15.3 + 2.0.9 + 5.10.1 + 5.8.0 + +``` + +Uses `` with BOM imports for AWS (`software.amazon.awssdk:bom`), Azure (`com.azure:azure-sdk-bom`), and GCP (`com.google.cloud:libraries-bom`). + +--- + +## 3. Interface Design (Java) + +### 3.1 Scala-to-Java Type Mapping + +| Scala Type | Java Type | Notes | +|-----------|----------|-------| +| `Option[Boolean]` | `boolean` parameter + method overloads with defaults | Convenience `default` methods | +| `Option[Int]` | `Integer` (nullable) | Null = not specified | +| `Option[String]` | `String` (nullable) | Null = not specified | +| `List[Blob]` | `java.util.List` | Immutable via `List.of()` / `Collections.unmodifiableList()` | +| `List[(String, Boolean)]` | `java.util.List` | New typed record instead of tuple | +| `Future[List[String]]` | `CompletableFuture>` | Standard Java async | +| `Array[Byte]` | `byte[]` | Direct | +| `Map[String, AnyRef]` | `Map` | Standard Java | +| `Option[Array[Byte]]` | `byte[]` (nullable) | Null = no payload | + +### 3.2 IStorageService Interface + +```java +public interface IStorageService extends AutoCloseable { + + // Upload + String upload(String container, String file, String objectKey, + boolean isDirectory, int attempt, int retryCount, Integer ttl); + default String upload(String container, String file, String objectKey) { ... } + + CompletableFuture> uploadFolder(String container, String file, + String objectKey, boolean isPublic, Integer ttl, + Integer retryCount, int attempt); + default CompletableFuture> uploadFolder(String container, + String file, String objectKey) { ... } + + String put(String container, byte[] content, String objectKey, + boolean isPublic, boolean isDirectory, Integer ttl, Integer retryCount); + default String put(String container, byte[] content, String objectKey) { ... } + + // Signed URLs + String getSignedURL(String container, String objectKey, Integer ttl, String permission); + default String getSignedURL(String container, String objectKey) { ... } + + String getSignedURLV2(String container, String objectKey, Integer ttl, + String permission, String contentType, + Map additionalParams); + default String getSignedURLV2(String container, String objectKey) { ... } + + // Download + void download(String container, String objectKey, String localPath, boolean isDirectory); + default void download(String container, String objectKey, String localPath) { ... } + + // Delete + void deleteObject(String container, String objectKey, boolean isDirectory); + default void deleteObject(String container, String objectKey) { ... } + void deleteObjects(String container, List objectKeys); + + // Copy + void copyObjects(String fromContainer, String fromKey, + String toContainer, String toKey, boolean isDirectory); + default void copyObjects(String from, String fKey, String to, String tKey) { ... } + + // Archive + void extractArchive(String container, String objectKey, String toKey); + + // Object access + Blob getObject(String container, String objectKey, boolean withPayload); + default Blob getObject(String container, String objectKey) { ... } + List getObjectData(String container, String objectKey); + List listObjects(String container, String prefix, boolean withPayload); + default List listObjects(String container, String prefix) { ... } + List listObjectKeys(String container, String prefix); + + // Search + List searchObjects(String container, String prefix, + String fromDate, String toDate, + Integer delta, String pattern); + List searchObjectKeys(String container, String prefix, + String fromDate, String toDate, + Integer delta, String pattern); + + // Path operations + List getPaths(String container, List objects); + String getUri(String container, String prefix, boolean isDirectory); + default String getUri(String container, String prefix) { ... } + + // Lifecycle + @Override void close(); +} +``` + +### 3.3 StorageConfig (Builder pattern with enums) + +```java +public final class StorageConfig { + + public enum AuthType { + ACCESS_KEY, + OIDC, // NEW: primary auth for cloud-native deployments + IAM, + IAM_ROLE, + INSTANCE_PROFILE + } + + public enum StorageType { + AWS, AZURE, GCLOUD, OCI, CEPHS3 + } + + // Fields: type, storageKey, storageSecret, endPoint, region, authType + // Builder: StorageConfig.builder(StorageType.AWS).authType(AuthType.OIDC).build() +} +``` + +### 3.4 Models + +**Blob.java** — Immutable data class with: `key`, `contentLength`, `lastModified`, `metadata`, `payload` (nullable byte[]) + +**DeleteTarget.java** — Replaces `(String, Boolean)` tuple with: `objectKey`, `isDirectory`. Static factories: `DeleteTarget.file(key)`, `DeleteTarget.directory(key)` + +### 3.5 StorageServiceFactory (ServiceLoader discovery) + +```java +public final class StorageServiceFactory { + public static IStorageService getStorageService(StorageConfig config) { + ServiceLoader loader = + ServiceLoader.load(StorageServiceProvider.class); + for (StorageServiceProvider provider : loader) { + if (provider.supports(config.getType())) { + return provider.create(config); + } + } + throw new StorageServiceException("No provider found for: " + config.getType()); + } +} +``` + +### 3.6 StorageServiceProvider (SPI interface) + +```java +public interface StorageServiceProvider { + boolean supports(StorageConfig.StorageType type); + IStorageService create(StorageConfig config); +} +``` + +Each CSP module registers its provider via `META-INF/services/org.sunbird.cloud.storage.StorageServiceProvider`. + +--- + +## 4. AbstractStorageService (Shared Logic in API Module) + +Migrates orchestration logic from `BaseStorageService.scala` to Java. This is ~50% of BaseStorageService. + +### Logic kept in abstract base (CSP-independent): +- `filesList()` — recursive file listing +- `uploadFolder()` — parallel upload via CompletableFuture + ForkJoinPool +- `upload()` — retry loop orchestration calling abstract `putObject()` +- `put()` — retry loop calling abstract `putContent()` +- `download()` — directory download orchestration calling abstract `downloadObject()` +- `deleteObject()` — delegates to `deleteObjects()` +- `searchObjects()` / `searchObjectKeys()` — date range iteration via `DateRangeUtil` +- `extractArchive()` — download → unzip → upload orchestration +- `listObjects()` — iterates `listObjectKeys` + `getObject` +- `copyObjects()` — directory copy orchestration calling abstract `copyObject()` + +### Abstract methods each CSP must implement: + +```java +// Container management +protected abstract void ensureContainerExists(String container); + +// Blob I/O +protected abstract String putObject(String container, String objectKey, File file); +protected abstract String putObject(String container, String objectKey, byte[] content); +protected abstract InputStream getObjectStream(String container, String objectKey); +protected abstract String getObjectUri(String container, String objectKey); +protected abstract BlobDetail getObjectDetail(String container, String objectKey); + +// Listing +protected abstract List listKeys(String container, String prefix); + +// Delete & Copy +protected abstract void removeObjects(String container, List objectKeys); +protected abstract void copyObject(String fromContainer, String fromKey, + String toContainer, String toKey); + +// Signed URLs +protected abstract String generateSignedGetUrl(String container, String objectKey, + int ttlSeconds); +protected abstract String generateSignedPutUrl(String container, String objectKey, + int ttlSeconds, String contentType, + Map additionalParams); + +// HDFS path prefix (e.g., "s3n://", "wasb://...", "gs://") +protected abstract String getHdfsPrefix(String container); +``` + +### Utility classes (in API module): + +**DateRangeUtil.java**: Replaces Joda-Time with `java.time.LocalDate` / `DateTimeFormatter` / `ChronoUnit`. + +**FileUtil.java**: `listFilesRecursive()`, `copyStream()`, `unZip()` — ported from `CommonUtil.scala`. + +--- + +## 5. CSP Module Implementations + +### 5.1 AWS Module (`cloud-storage-sdk-aws`) + +**SDK**: AWS SDK v2 (`software.amazon.awssdk:s3`, `software.amazon.awssdk:sts`) + +**Key class**: `AwsStorageService extends AbstractStorageService` + +**Credential resolution by AuthType**: +| AuthType | AWS Credential Provider | +|----------|------------------------| +| `ACCESS_KEY` | `StaticCredentialsProvider.create(AwsBasicCredentials.create(key, secret))` | +| `OIDC` | `StsWebIdentityTokenFileCredentialsProvider` (reads `AWS_WEB_IDENTITY_TOKEN_FILE`, `AWS_ROLE_ARN`) | +| `IAM` / `IAM_ROLE` / `INSTANCE_PROFILE` | `DefaultCredentialsProvider.create()` | + +**Ceph S3 support**: When `StorageType.CEPHS3`, builds `S3Client` with: +- `endpointOverride(URI.create(config.getEndPoint()))` +- `S3Configuration.builder().pathStyleAccessEnabled(true).build()` + +**Signed URLs**: Uses `S3Presigner` with `presignGetObject()` / `presignPutObject()` + +**HDFS prefix**: `s3n:///` + +**AwsStorageServiceProvider**: `supports()` returns `true` for `StorageType.AWS` and `StorageType.CEPHS3` + +### 5.2 Azure Module (`cloud-storage-sdk-azure`) + +**SDK**: `com.azure:azure-storage-blob`, `com.azure:azure-identity` + +Replaces the old `com.microsoft.azure:azure-storage:5.0.0` with the modern Azure SDK for Java. + +**Key class**: `AzureStorageService extends AbstractStorageService` + +**Credential resolution by AuthType**: +| AuthType | Azure Credential | +|----------|-----------------| +| `ACCESS_KEY` | `StorageSharedKeyCredential(accountName, accountKey)` | +| `OIDC` | `WorkloadIdentityCredentialBuilder` (reads `AZURE_CLIENT_ID`, `AZURE_TENANT_ID`, `AZURE_FEDERATED_TOKEN_FILE`) | +| `IAM` / `INSTANCE_PROFILE` | `ManagedIdentityCredentialBuilder` | + +**Signed URLs**: Uses SAS (Shared Access Signatures): +- Access key auth: `blobClient.generateSas(sasValues)` +- OIDC/IAM auth: User Delegation SAS via `blobServiceClient.getUserDelegationKey()` + `blobClient.generateUserDelegationSas()` + +**HDFS prefix**: `wasb://@.blob.core.windows.net/` + +### 5.3 GCP Module (`cloud-storage-sdk-gcp`) + +**SDK**: `com.google.cloud:google-cloud-storage` + +The current GCP implementation already works around jclouds by using native GCP SDK for signed URLs and raw HTTP for `listObjectKeys` and `download`. The new module eliminates jclouds entirely and uses native SDK for everything. + +**Key class**: `GcpStorageService extends AbstractStorageService` + +**Credential resolution by AuthType**: +| AuthType | GCP Credential | +|----------|---------------| +| `ACCESS_KEY` | `ServiceAccountCredentials.fromPkcs8()` using `additionalParams` from config, or `ServiceAccountCredentials.fromStream()` | +| `OIDC` | `ExternalAccountCredentials` via Workload Identity Federation (`GOOGLE_APPLICATION_CREDENTIALS` env var) | +| `IAM` / `INSTANCE_PROFILE` | `GoogleCredentials.getApplicationDefault()` / `ComputeEngineCredentials` | + +**Signed URLs** (ported from current `GcloudStorageService.getPutSignedURL()`): +```java +storage.signUrl(blobInfo, ttl, TimeUnit.SECONDS, + Storage.SignUrlOption.httpMethod(method), + Storage.SignUrlOption.withV4Signature()); +``` +Supports chunked/resumable uploads via `HttpMethod.POST` + `x-goog-resumable: start` header (matching current implementation). + +**HDFS prefix**: `gs:///` + +### 5.4 OCI Module (`cloud-storage-sdk-oci`) + +**SDK**: `com.oracle.oci.sdk:oci-java-sdk-objectstorage` (native OCI API) + +**Key class**: `OciStorageService extends AbstractStorageService` + +**Credential resolution by AuthType**: +| AuthType | OCI Credential Provider | +|----------|------------------------| +| `ACCESS_KEY` | `SimpleAuthenticationDetailsProvider` (API key-based auth with tenancy/user/fingerprint/key) | +| `OIDC` | `OkeWorkloadIdentityAuthenticationDetailsProvider` (for OKE Kubernetes workloads) | +| `IAM` / `INSTANCE_PROFILE` | `InstancePrincipalsAuthenticationDetailsProvider` | + +**Key differences from current S3-compatible approach**: +- Uses `ObjectStorageClient` instead of S3 client with endpoint override +- Native pre-authenticated request (PAR) API for signed URLs via `CreatePreauthenticatedRequestRequest` +- Uses `PutObjectRequest` with MD5 hash (matching current OCI behavior) +- Requires `namespace` parameter (OCI-specific) — added to `StorageConfig` as optional field or derived via `ObjectStorageClient.getNamespace()` + +**HDFS prefix**: `oci://@/` (OCI native) or configurable + +--- + +## 6. Authentication Architecture + +### OIDC as Primary Auth + +OIDC-based auth is the default/primary approach for cloud-native Kubernetes deployments: + +| CSP | OIDC Mechanism | Environment Variables | +|-----|---------------|----------------------| +| AWS | STS AssumeRoleWithWebIdentity | `AWS_WEB_IDENTITY_TOKEN_FILE`, `AWS_ROLE_ARN`, `AWS_ROLE_SESSION_NAME` | +| Azure | Workload Identity Federation | `AZURE_CLIENT_ID`, `AZURE_TENANT_ID`, `AZURE_FEDERATED_TOKEN_FILE` | +| GCP | Workload Identity Federation | `GOOGLE_APPLICATION_CREDENTIALS` (credential config JSON) | +| OCI | OKE Workload Identity | OCI-specific service account token | + +### Configuration Example + +```java +// OIDC auth (new primary approach — no keys needed) +StorageConfig config = StorageConfig.builder(StorageConfig.StorageType.AWS) + .authType(StorageConfig.AuthType.OIDC) + .region("us-east-1") + .build(); + +// Access Key auth (configurable fallback) +StorageConfig config = StorageConfig.builder(StorageConfig.StorageType.AWS) + .authType(StorageConfig.AuthType.ACCESS_KEY) + .storageKey("AKIA...") + .storageSecret("secret...") + .build(); + +// Factory creates the right service via ServiceLoader +IStorageService service = StorageServiceFactory.getStorageService(config); +``` + +--- + +## 7. Implementation Phases + +### Phase 1: Project Structure + API Module +1. Create parent POM with multi-module structure +2. Remove all existing Scala source code and Scala-related build configuration +3. Create `cloud-storage-sdk-api` module POM +4. Implement `IStorageService.java` interface +5. Implement `Blob.java`, `DeleteTarget.java`, `StorageServiceException.java` +6. Implement `StorageConfig.java` with Builder pattern and enums +7. Implement `StorageServiceProvider.java` SPI interface +8. Implement `StorageServiceFactory.java` with ServiceLoader discovery +9. Implement `DateRangeUtil.java` (port from Joda-Time to java.time) +10. Implement `FileUtil.java` (port from CommonUtil.scala) +11. Implement `AbstractStorageService.java` with shared orchestration logic + +### Phase 2: AWS Module +1. Create `cloud-storage-sdk-aws` module POM +2. Implement `AwsStorageService.java` with AWS SDK v2 S3Client / S3Presigner +3. Implement credential resolution (access key, OIDC via STS, IAM) +4. Handle Ceph S3 (endpoint override + path-style access) +5. Implement `AwsStorageServiceProvider.java` + META-INF/services registration +6. Write unit tests + +### Phase 3: Azure Module +1. Create `cloud-storage-sdk-azure` module POM +2. Implement `AzureStorageService.java` with BlobServiceClient +3. Implement credential resolution (shared key, workload identity, managed identity) +4. Implement SAS token generation for signed URLs +5. Implement `AzureStorageServiceProvider.java` + META-INF/services registration +6. Write unit tests + +### Phase 4: GCP Module +1. Create `cloud-storage-sdk-gcp` module POM +2. Implement `GcpStorageService.java` with native com.google.cloud.storage.Storage +3. Implement credential resolution (service account, WIF, application default) +4. Port V4 signed URL logic from current `GcloudStorageService.getPutSignedURL()` +5. Implement `GcpStorageServiceProvider.java` + META-INF/services registration +6. Write unit tests + +### Phase 5: OCI Module +1. Create `cloud-storage-sdk-oci` module POM +2. Implement `OciStorageService.java` with OCI ObjectStorageClient +3. Implement credential resolution (API key, instance principal, OKE workload identity) +4. Handle OCI-specific concerns (namespace, MD5 on upload, pre-authenticated requests) +5. Implement `OciStorageServiceProvider.java` + META-INF/services registration +6. Write unit tests + +### Phase 6: CI/CD and Release +1. Update `.github/workflows/maven-release.yml` for multi-module build +2. Update `.circleci/config.yml` for multi-module build and SonarCloud analysis +3. Configure individual module publishing to Maven Central +4. Update README with architecture overview and migration guide + +--- + +## 8. Consumer Migration Guide + +### Dependency Change + +**Before:** +```xml + + org.sunbird + cloud-store-sdk_2.13 + 1.4.10 + +``` + +**After (e.g., for AWS):** +```xml + + org.sunbird + cloud-storage-sdk-api + 2.0.0 + + + org.sunbird + cloud-storage-sdk-aws + 2.0.0 + runtime + +``` + +### Key API Changes + +| Before (Scala) | After (Java) | +|----------------|-------------| +| `Option(false)` parameters | `boolean` params with `default` method overloads | +| `service.closeContext()` | `service.close()` (AutoCloseable / try-with-resources) | +| `StorageConfig("aws", key, secret)` | `StorageConfig.builder(AWS).storageKey(key).storageSecret(secret).build()` | +| `List[(String, Boolean)]` | `List` | +| `searchObjectkeys` (lowercase k) | `searchObjectKeys` (camelCase) | +| `import o.s.c.s.factory.StorageServiceFactory` | `import o.s.c.s.StorageServiceFactory` (package changed) | + +### Scala consumers +Since the new modules are pure Java JARs, Scala consumers can use them directly without Scala version cross-compilation issues: +```scala +import org.sunbird.cloud.storage._ +val config = StorageConfig.builder(StorageConfig.StorageType.AWS) + .authType(StorageConfig.AuthType.OIDC) + .build() +val service = StorageServiceFactory.getStorageService(config) +``` diff --git a/README.md b/README.md index 995bf01..8068060 100644 --- a/README.md +++ b/README.md @@ -1 +1,94 @@ -# sunbird-cloud-storage \ No newline at end of file +# Sunbird Cloud Storage SDK + +Multi-module Java 11 SDK providing cloud storage operations (upload, download, signed URLs, delete, copy, search, etc.) with CSP-specific implementations using official cloud SDKs. + +## Modules + +| Module | Artifact ID | Description | +|--------|-------------|-------------| +| API | `cloud-storage-sdk-api` | Interface, models, factory, and shared abstractions | +| AWS | `cloud-storage-sdk-aws` | AWS S3 + Ceph S3-compatible (AWS SDK v2) | +| Azure | `cloud-storage-sdk-azure` | Azure Blob Storage (Azure SDK for Java) | +| GCP | `cloud-storage-sdk-gcp` | Google Cloud Storage (Google Cloud Java SDK) | +| OCI | `cloud-storage-sdk-oci` | Oracle Cloud Object Storage (OCI Java SDK) | + +## Authentication + +OIDC-based authentication is the primary approach for cloud-native deployments. Access Key authentication is available as a configurable fallback. + +| Auth Type | Description | +|-----------|-------------| +| `OIDC` | Workload Identity Federation / Web Identity Token (primary) | +| `ACCESS_KEY` | Static access key and secret | +| `IAM` | IAM role / managed identity | +| `INSTANCE_PROFILE` | Cloud instance metadata credentials | + +## Usage + +### Maven Dependency + +Add the API module and one CSP-specific module: + +```xml + + org.sunbird + cloud-storage-sdk-api + 2.0.0 + + + + + org.sunbird + cloud-storage-sdk-aws + 2.0.0 + runtime + +``` + +### Java Example + +```java +import org.sunbird.cloud.storage.*; + +// OIDC auth (cloud-native, no keys needed) +StorageConfig config = StorageConfig.builder(StorageConfig.StorageType.AWS) + .authType(StorageConfig.AuthType.OIDC) + .region("us-east-1") + .build(); + +// Access Key auth (fallback) +StorageConfig config = StorageConfig.builder(StorageConfig.StorageType.AWS) + .authType(StorageConfig.AuthType.ACCESS_KEY) + .storageKey("AKIA...") + .storageSecret("secret...") + .build(); + +// Factory auto-discovers the CSP module via ServiceLoader +try (IStorageService service = StorageServiceFactory.getStorageService(config)) { + // Upload + String url = service.upload("my-bucket", "/local/file.txt", "path/file.txt"); + + // Signed URL + String signedUrl = service.getSignedURL("my-bucket", "path/file.txt", 3600, "r"); + + // Download + service.download("my-bucket", "path/file.txt", "/local/download/"); + + // List + List keys = service.listObjectKeys("my-bucket", "path/"); +} +``` + +## Building + +```bash +mvn clean install +``` + +## Migration from v1.x + +See [PLAN.md](PLAN.md) for the detailed migration guide from the Scala/jclouds-based v1.x. + +## License + +[MIT License](LICENSE) diff --git a/cloud-storage-sdk-api/pom.xml b/cloud-storage-sdk-api/pom.xml new file mode 100644 index 0000000..787e4fe --- /dev/null +++ b/cloud-storage-sdk-api/pom.xml @@ -0,0 +1,36 @@ + + 4.0.0 + + org.sunbird + cloud-storage-sdk-parent + 2.0.0 + + + cloud-storage-sdk-api + jar + Cloud Storage SDK API + Interface definitions, models, and shared abstractions for cloud storage operations. + + + + org.slf4j + slf4j-api + + + org.apache.tika + tika-core + + + org.junit.jupiter + junit-jupiter + test + + + org.mockito + mockito-core + test + + + diff --git a/cloud-storage-sdk-api/src/main/java/org/sunbird/cloud/storage/AbstractStorageService.java b/cloud-storage-sdk-api/src/main/java/org/sunbird/cloud/storage/AbstractStorageService.java new file mode 100644 index 0000000..bf39413 --- /dev/null +++ b/cloud-storage-sdk-api/src/main/java/org/sunbird/cloud/storage/AbstractStorageService.java @@ -0,0 +1,425 @@ +package org.sunbird.cloud.storage; + +import org.apache.tika.Tika; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.sunbird.cloud.storage.exception.StorageServiceException; +import org.sunbird.cloud.storage.model.Blob; +import org.sunbird.cloud.storage.model.DeleteTarget; +import org.sunbird.cloud.storage.util.DateRangeUtil; +import org.sunbird.cloud.storage.util.FileUtil; + +import java.io.BufferedReader; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.stream.Collectors; + +/** + * Abstract base implementation of {@link IStorageService} providing shared orchestration logic. + * + *

CSP-specific subclasses implement the abstract primitive methods to interact with + * the actual cloud storage APIs.

+ */ +public abstract class AbstractStorageService implements IStorageService { + + private static final Logger logger = LoggerFactory.getLogger(AbstractStorageService.class); + + protected final Tika tika = new Tika(); + protected int maxRetries = 2; + protected int maxSignedUrlTTL = 604800; + + // --- Abstract primitives that each CSP module must implement --- + + protected abstract void ensureContainerExists(String container); + + protected abstract String putObject(String container, String objectKey, File file); + + protected abstract String putObject(String container, String objectKey, byte[] content); + + protected abstract InputStream getObjectStream(String container, String objectKey); + + protected abstract String getObjectUri(String container, String objectKey); + + protected abstract BlobDetail getObjectDetail(String container, String objectKey); + + protected abstract List listKeys(String container, String prefix); + + protected abstract void removeObjects(String container, List objectKeys); + + protected abstract void copyObject(String fromContainer, String fromKey, + String toContainer, String toKey); + + protected abstract String generateSignedGetUrl(String container, String objectKey, + int ttlSeconds); + + protected abstract String generateSignedPutUrl(String container, String objectKey, + int ttlSeconds, String contentType, + Map additionalParams); + + /** + * Get the HDFS path prefix for this CSP (e.g., "s3n://", "gs://", "wasb://..."). + * + * @param container the container/bucket name + * @return the full HDFS prefix including the container + */ + protected abstract String getHdfsPrefix(String container); + + /** + * Metadata holder returned by {@link #getObjectDetail(String, String)}. + */ + protected static class BlobDetail { + public final String key; + public final long contentLength; + public final Date lastModified; + public final Map metadata; + public final String uri; + + public BlobDetail(String key, long contentLength, Date lastModified, + Map metadata, String uri) { + this.key = key; + this.contentLength = contentLength; + this.lastModified = lastModified; + this.metadata = metadata; + this.uri = uri; + } + } + + // --- Orchestration implementations --- + + @Override + public String upload(String container, String file, String objectKey, + boolean isDirectory, int attempt, int retryCount, Integer ttl) { + int maxAttempts = retryCount > 0 ? retryCount : maxRetries; + try { + if (isDirectory) { + File dir = new File(file); + List files = FileUtil.listFilesRecursive(dir); + List urls = new ArrayList<>(); + for (File f : files) { + String key = objectKey + f.getAbsolutePath() + .substring((dir.getAbsolutePath() + File.separator).length()); + urls.add(upload(container, f.getAbsolutePath(), key, false, attempt, maxAttempts, ttl)); + } + return String.join(",", urls); + } else { + if (attempt >= maxAttempts) { + throw new StorageServiceException( + "Failed to upload. file: " + file + ", key: " + objectKey + + ", attempt: " + attempt + ", maxAttempts: " + maxAttempts + + ". Exceeded maximum number of retries"); + } + ensureContainerExists(container); + String uri = putObject(container, objectKey, new File(file)); + if (ttl != null) { + return getSignedURL(container, objectKey, ttl, "r"); + } + return uri; + } + } catch (StorageServiceException e) { + throw e; + } catch (Exception e) { + logger.error("Upload failed for key: {}, attempt: {}", objectKey, attempt, e); + try { + Thread.sleep((long) attempt * 2000); + } catch (InterruptedException ie) { + Thread.currentThread().interrupt(); + } + int nextAttempt = attempt + 1; + if (nextAttempt <= maxAttempts) { + return upload(container, file, objectKey, isDirectory, nextAttempt, maxAttempts, ttl); + } + throw new StorageServiceException("Upload failed after " + attempt + " attempts: " + e.getMessage(), e); + } + } + + @Override + public CompletableFuture> uploadFolder(String container, String file, + String objectKey, boolean isPublic, + Integer ttl, Integer retryCount, + int attempt) { + return CompletableFuture.supplyAsync(() -> { + File dir = new File(file); + List files = FileUtil.listFilesRecursive(dir); + List> futures = new ArrayList<>(); + for (File f : files) { + String key = objectKey + f.getAbsolutePath() + .substring((dir.getAbsolutePath() + File.separator).length()); + futures.add(CompletableFuture.supplyAsync(() -> + upload(container, f.getAbsolutePath(), key, false, attempt, + retryCount != null ? retryCount : 0, ttl))); + } + return futures.stream() + .map(CompletableFuture::join) + .collect(Collectors.toList()); + }); + } + + @Override + public String put(String container, byte[] content, String objectKey, + boolean isPublic, boolean isDirectory, Integer ttl, Integer retryCount) { + int maxAttempts = retryCount != null ? retryCount : maxRetries; + int attempt = 0; + while (true) { + try { + if (attempt >= maxAttempts) { + throw new StorageServiceException( + "Failed to put. key: " + objectKey + + ", attempt: " + attempt + ", maxAttempts: " + maxAttempts + + ". Exceeded maximum number of retries"); + } + ensureContainerExists(container); + String uri = putObject(container, objectKey, content); + if (isPublic) { + return getSignedURL(container, objectKey, + ttl != null ? ttl : maxSignedUrlTTL, "r"); + } + return uri; + } catch (StorageServiceException e) { + throw e; + } catch (Exception e) { + logger.error("Put failed for key: {}, attempt: {}", objectKey, attempt, e); + try { + Thread.sleep((long) attempt * 2000); + } catch (InterruptedException ie) { + Thread.currentThread().interrupt(); + } + attempt++; + if (attempt > maxAttempts) { + throw new StorageServiceException("Put failed: " + e.getMessage(), e); + } + } + } + } + + @Override + public String getSignedURL(String container, String objectKey, Integer ttl, String permission) { + int effectiveTtl = ttl != null ? ttl : maxSignedUrlTTL; + if ("w".equalsIgnoreCase(permission)) { + return generateSignedPutUrl(container, objectKey, effectiveTtl, null, null); + } + return generateSignedGetUrl(container, objectKey, effectiveTtl); + } + + @Override + public String getSignedURLV2(String container, String objectKey, Integer ttl, + String permission, String contentType, + Map additionalParams) { + int effectiveTtl = ttl != null ? ttl : maxSignedUrlTTL; + if ("w".equalsIgnoreCase(permission)) { + return generateSignedPutUrl(container, objectKey, effectiveTtl, contentType, additionalParams); + } + return generateSignedGetUrl(container, objectKey, effectiveTtl); + } + + @Override + public void download(String container, String objectKey, String localPath, boolean isDirectory) { + try { + if (isDirectory) { + List objects = listObjectKeys(container, objectKey); + for (String obj : objects) { + InputStream stream = getObjectStream(container, obj); + String relativePath = obj.startsWith(objectKey) ? obj.substring(objectKey.length()) : obj; + File targetFile = new File(localPath, relativePath); + File parentDir = targetFile.getParentFile(); + if (parentDir != null && !parentDir.exists()) { + parentDir.mkdirs(); + } + String fileName = targetFile.getName(); + String dirPath = parentDir != null ? parentDir.getAbsolutePath() + "/" : localPath; + FileUtil.copyStream(stream, dirPath, fileName); + } + } else { + InputStream stream = getObjectStream(container, objectKey); + String fileName = objectKey.contains("/") + ? objectKey.substring(objectKey.lastIndexOf('/') + 1) + : objectKey; + FileUtil.copyStream(stream, localPath, fileName); + } + } catch (StorageServiceException e) { + throw e; + } catch (Exception e) { + throw new StorageServiceException("Download failed: " + e.getMessage(), e); + } + } + + @Override + public void deleteObject(String container, String objectKey, boolean isDirectory) { + deleteObjects(container, List.of(new DeleteTarget(objectKey, isDirectory))); + } + + @Override + public void deleteObjects(String container, List objectKeys) { + try { + for (DeleteTarget target : objectKeys) { + if (target.isDirectory()) { + List keys = listKeys(container, target.getObjectKey()); + if (!keys.isEmpty()) { + removeObjects(container, keys); + } + } else { + removeObjects(container, List.of(target.getObjectKey())); + } + } + } catch (StorageServiceException e) { + throw e; + } catch (Exception e) { + throw new StorageServiceException("Delete failed: " + e.getMessage(), e); + } + } + + @Override + public Blob getObject(String container, String objectKey, boolean withPayload) { + try { + BlobDetail detail = getObjectDetail(container, objectKey); + byte[] payload = null; + if (withPayload) { + try (InputStream is = getObjectStream(container, objectKey)) { + payload = is.readAllBytes(); + } + } + return new Blob(objectKey, detail.contentLength, detail.lastModified, + detail.metadata, payload); + } catch (StorageServiceException e) { + throw e; + } catch (Exception e) { + throw new StorageServiceException("Failed to get object: " + e.getMessage(), e); + } + } + + @Override + public List getObjectData(String container, String objectKey) { + try (InputStream is = getObjectStream(container, objectKey); + BufferedReader reader = new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8))) { + return reader.lines().collect(Collectors.toList()); + } catch (IOException e) { + throw new StorageServiceException("Failed to read object data: " + e.getMessage(), e); + } + } + + @Override + public List listObjects(String container, String prefix, boolean withPayload) { + try { + List keys = listObjectKeys(container, prefix); + List blobs = new ArrayList<>(); + for (String key : keys) { + blobs.add(getObject(container, key, withPayload)); + } + return blobs; + } catch (StorageServiceException e) { + throw e; + } catch (Exception e) { + throw new StorageServiceException("Failed to list objects: " + e.getMessage(), e); + } + } + + @Override + public List listObjectKeys(String container, String prefix) { + return listKeys(container, prefix); + } + + @Override + public List searchObjects(String container, String prefix, + String fromDate, String toDate, + Integer delta, String pattern) { + String from = (delta != null) ? DateRangeUtil.getStartDate(toDate, delta) : fromDate; + if (from != null && !from.isEmpty()) { + String[] dates = DateRangeUtil.getDatesBetween(from, toDate, pattern); + List results = new ArrayList<>(); + for (String date : dates) { + results.addAll(listObjects(container, prefix + date)); + } + return results; + } + return listObjects(container, prefix); + } + + @Override + public List searchObjectKeys(String container, String prefix, + String fromDate, String toDate, + Integer delta, String pattern) { + String from = (delta != null) ? DateRangeUtil.getStartDate(toDate, delta) : fromDate; + if (from != null && !from.isEmpty()) { + String[] dates = DateRangeUtil.getDatesBetween(from, toDate, pattern); + List results = new ArrayList<>(); + for (String date : dates) { + results.addAll(listObjectKeys(container, prefix + date)); + } + return results; + } + return listObjectKeys(container, prefix); + } + + @Override + public void copyObjects(String fromContainer, String fromKey, + String toContainer, String toKey, boolean isDirectory) { + if (isDirectory) { + String updatedFromKey = fromKey.endsWith("/") ? fromKey : fromKey + "/"; + String updatedToKey = toKey.endsWith("/") ? toKey : toKey + "/"; + List keys = listObjectKeys(fromContainer, updatedFromKey); + for (String key : keys) { + String objName = key.replace(updatedFromKey, ""); + copyObject(fromContainer, key, toContainer, updatedToKey + objName); + } + } else { + copyObject(fromContainer, fromKey, toContainer, toKey); + } + } + + @Override + public void extractArchive(String container, String objectKey, String toKey) { + try { + String localExtractPath = System.getProperty("local_extract_path", + System.getenv().getOrDefault("local_extract_path", "/tmp/extract")); + download(container, objectKey, localExtractPath, false); + String archiveName = objectKey.contains("/") + ? objectKey.substring(objectKey.lastIndexOf('/') + 1) + : objectKey; + String folderName = toKey.contains("/") + ? toKey.substring(toKey.lastIndexOf('/') + 1) + : toKey; + String localFolder = localExtractPath + "/" + folderName; + FileUtil.unZip(localExtractPath + "/" + archiveName, localFolder); + upload(container, localFolder, toKey, true, 1, 0, null); + } catch (StorageServiceException e) { + throw e; + } catch (Exception e) { + throw new StorageServiceException("Extract archive failed: " + e.getMessage(), e); + } + } + + @Override + public List getPaths(String container, List objects) { + String hdfsPrefix = getHdfsPrefix(container); + List paths = new ArrayList<>(); + for (Blob blob : objects) { + paths.add(hdfsPrefix + blob.getKey()); + } + return paths; + } + + @Override + public String getUri(String container, String prefix, boolean isDirectory) { + List keys = listObjectKeys(container, prefix); + if (keys.isEmpty()) { + throw new StorageServiceException("The given prefix is incorrect: " + prefix); + } + String firstKey = keys.get(0); + String uri = getObjectUri(container, firstKey); + if (uri != null && !uri.isEmpty()) { + int idx = uri.indexOf(prefix); + if (idx >= 0) { + return uri.substring(0, idx) + prefix; + } + return uri; + } + throw new StorageServiceException("URI not available for the given prefix: " + prefix); + } +} diff --git a/cloud-storage-sdk-api/src/main/java/org/sunbird/cloud/storage/IStorageService.java b/cloud-storage-sdk-api/src/main/java/org/sunbird/cloud/storage/IStorageService.java new file mode 100644 index 0000000..c9ac4e9 --- /dev/null +++ b/cloud-storage-sdk-api/src/main/java/org/sunbird/cloud/storage/IStorageService.java @@ -0,0 +1,302 @@ +package org.sunbird.cloud.storage; + +import org.sunbird.cloud.storage.model.Blob; +import org.sunbird.cloud.storage.model.DeleteTarget; + +import java.util.List; +import java.util.Map; +import java.util.concurrent.CompletableFuture; + +/** + * Cloud storage service interface providing operations for upload, download, + * signed URL generation, deletion, copy, search, and metadata retrieval. + * + *

Implementations are CSP-specific (AWS, Azure, GCP, OCI) and discovered + * via {@link StorageServiceFactory} using Java ServiceLoader.

+ */ +public interface IStorageService extends AutoCloseable { + + // --- Upload operations --- + + /** + * Upload a file or folder to cloud storage. + * + * @param container the container/bucket name + * @param file the local file path + * @param objectKey the destination key/path in cloud storage + * @param isDirectory whether to upload recursively as a directory + * @param attempt current attempt number (starting from 1) + * @param retryCount maximum number of retries before failing + * @param ttl time-to-live in seconds for the uploaded object (null for no expiry) + * @return the URL of the uploaded file + */ + String upload(String container, String file, String objectKey, + boolean isDirectory, int attempt, int retryCount, Integer ttl); + + default String upload(String container, String file, String objectKey) { + return upload(container, file, objectKey, false, 1, 0, null); + } + + /** + * Upload a folder asynchronously. + * + * @param container the container/bucket name + * @param file the local folder path + * @param objectKey the destination prefix in cloud storage + * @param isPublic whether uploaded files should have public read access + * @param ttl time-to-live in seconds (null for no expiry) + * @param retryCount maximum number of retries (null for default) + * @param attempt current attempt number + * @return future completing with a list of uploaded file URLs + */ + CompletableFuture> uploadFolder(String container, String file, + String objectKey, boolean isPublic, + Integer ttl, Integer retryCount, int attempt); + + default CompletableFuture> uploadFolder(String container, String file, + String objectKey) { + return uploadFolder(container, file, objectKey, false, null, null, 1); + } + + /** + * Put binary content directly as a blob in cloud storage. + * + * @param container the container/bucket name + * @param content the byte array payload + * @param objectKey the destination key/path + * @param isPublic whether the object should have public read access + * @param isDirectory whether the content represents a directory + * @param ttl time-to-live in seconds (null for no expiry) + * @param retryCount maximum number of retries (null for default) + * @return the URL of the uploaded object + */ + String put(String container, byte[] content, String objectKey, + boolean isPublic, boolean isDirectory, Integer ttl, Integer retryCount); + + default String put(String container, byte[] content, String objectKey) { + return put(container, content, objectKey, false, false, null, null); + } + + // --- Signed URL operations --- + + /** + * Get a pre-signed URL to access an object. + * + * @param container the container/bucket name + * @param objectKey the object key + * @param ttl time-to-live in seconds for the URL (null for default) + * @param permission "r" for read, "w" for write + * @return the pre-signed URL + */ + String getSignedURL(String container, String objectKey, Integer ttl, String permission); + + default String getSignedURL(String container, String objectKey) { + return getSignedURL(container, objectKey, null, "r"); + } + + /** + * Get a pre-signed URL with additional options (content type, extra params). + * + * @param container the container/bucket name + * @param objectKey the object key + * @param ttl time-to-live in seconds (null for default) + * @param permission "r" for read, "w" for write + * @param contentType the content type of the object + * @param additionalParams CSP-specific additional parameters (nullable) + * @return the pre-signed URL + */ + String getSignedURLV2(String container, String objectKey, Integer ttl, + String permission, String contentType, + Map additionalParams); + + default String getSignedURLV2(String container, String objectKey) { + return getSignedURLV2(container, objectKey, null, "r", "application/octet-stream", null); + } + + // --- Download operations --- + + /** + * Download a file or folder from cloud storage. + * + * @param container the container/bucket name + * @param objectKey the object key to download + * @param localPath the local destination path + * @param isDirectory whether to download recursively as a directory + */ + void download(String container, String objectKey, String localPath, boolean isDirectory); + + default void download(String container, String objectKey, String localPath) { + download(container, objectKey, localPath, false); + } + + // --- Delete operations --- + + /** + * Delete an object from cloud storage. + * + * @param container the container/bucket name + * @param objectKey the object key to delete + * @param isDirectory whether the object is a directory (recursive delete) + */ + void deleteObject(String container, String objectKey, boolean isDirectory); + + default void deleteObject(String container, String objectKey) { + deleteObject(container, objectKey, false); + } + + /** + * Delete multiple objects from cloud storage. + * + * @param container the container/bucket name + * @param objectKeys list of delete targets + */ + void deleteObjects(String container, List objectKeys); + + // --- Copy operations --- + + /** + * Copy objects between containers or within the same container. + * + * @param fromContainer source container + * @param fromKey source object key/prefix + * @param toContainer destination container + * @param toKey destination object key/prefix + * @param isDirectory whether to copy recursively as a directory + */ + void copyObjects(String fromContainer, String fromKey, + String toContainer, String toKey, boolean isDirectory); + + default void copyObjects(String fromContainer, String fromKey, + String toContainer, String toKey) { + copyObjects(fromContainer, fromKey, toContainer, toKey, false); + } + + // --- Archive operations --- + + /** + * Extract an archive file on cloud storage to a destination folder. + * + * @param container the container/bucket name + * @param objectKey the archive object key + * @param toKey the destination folder key + */ + void extractArchive(String container, String objectKey, String toKey); + + // --- Object retrieval --- + + /** + * Get blob object details. + * + * @param container the container/bucket name + * @param objectKey the object key + * @param withPayload whether to include the payload bytes + * @return the blob object with metadata + */ + Blob getObject(String container, String objectKey, boolean withPayload); + + default Blob getObject(String container, String objectKey) { + return getObject(container, objectKey, false); + } + + /** + * Get object data as lines of text. + * + * @param container the container/bucket name + * @param objectKey the object key + * @return the object content as a list of lines + */ + List getObjectData(String container, String objectKey); + + /** + * List blob objects for a given prefix. + * + * @param container the container/bucket name + * @param prefix the object prefix to list + * @param withPayload whether to include payloads + * @return list of blob objects + */ + List listObjects(String container, String prefix, boolean withPayload); + + default List listObjects(String container, String prefix) { + return listObjects(container, prefix, false); + } + + /** + * List object keys for a given prefix. + * + * @param container the container/bucket name + * @param prefix the object prefix + * @return list of object keys + */ + List listObjectKeys(String container, String prefix); + + // --- Search operations --- + + /** + * Search for objects by date-prefixed paths. + * + * @param container the container/bucket name + * @param prefix the base prefix + * @param fromDate start date string (nullable) + * @param toDate end date string (nullable) + * @param delta number of days delta from toDate (nullable) + * @param pattern date format pattern (e.g., "yyyy-MM-dd") + * @return list of matching blob objects + */ + List searchObjects(String container, String prefix, + String fromDate, String toDate, + Integer delta, String pattern); + + default List searchObjects(String container, String prefix) { + return searchObjects(container, prefix, null, null, null, "yyyy-MM-dd"); + } + + /** + * Search for object keys by date-prefixed paths. + * + * @param container the container/bucket name + * @param prefix the base prefix + * @param fromDate start date string (nullable) + * @param toDate end date string (nullable) + * @param delta number of days delta from toDate (nullable) + * @param pattern date format pattern (e.g., "yyyy-MM-dd") + * @return list of matching object keys + */ + List searchObjectKeys(String container, String prefix, + String fromDate, String toDate, + Integer delta, String pattern); + + default List searchObjectKeys(String container, String prefix) { + return searchObjectKeys(container, prefix, null, null, null, "yyyy-MM-dd"); + } + + // --- Path operations --- + + /** + * Get HDFS-compatible file paths for use with Spark etc. + * + * @param container the container/bucket name + * @param objects list of blob objects + * @return HDFS-compatible paths + */ + List getPaths(String container, List objects); + + /** + * Get the URI for a given prefix. + * + * @param container the container/bucket name + * @param prefix the object prefix + * @param isDirectory whether the prefix represents a directory + * @return the URI string + */ + String getUri(String container, String prefix, boolean isDirectory); + + default String getUri(String container, String prefix) { + return getUri(container, prefix, false); + } + + // --- Lifecycle --- + + @Override + void close(); +} diff --git a/cloud-storage-sdk-api/src/main/java/org/sunbird/cloud/storage/StorageConfig.java b/cloud-storage-sdk-api/src/main/java/org/sunbird/cloud/storage/StorageConfig.java new file mode 100644 index 0000000..a2aa210 --- /dev/null +++ b/cloud-storage-sdk-api/src/main/java/org/sunbird/cloud/storage/StorageConfig.java @@ -0,0 +1,106 @@ +package org.sunbird.cloud.storage; + +/** + * Configuration for cloud storage service initialization. + * Use the {@link Builder} pattern for construction. + */ +public final class StorageConfig { + + public enum AuthType { + ACCESS_KEY, + OIDC, + IAM, + IAM_ROLE, + INSTANCE_PROFILE + } + + public enum StorageType { + AWS, AZURE, GCLOUD, OCI, CEPHS3 + } + + private final StorageType type; + private final String storageKey; + private final String storageSecret; + private final String endPoint; + private final String region; + private final AuthType authType; + + private StorageConfig(Builder builder) { + this.type = builder.type; + this.storageKey = builder.storageKey; + this.storageSecret = builder.storageSecret; + this.endPoint = builder.endPoint; + this.region = builder.region; + this.authType = builder.authType; + } + + public StorageType getType() { + return type; + } + + public String getStorageKey() { + return storageKey; + } + + public String getStorageSecret() { + return storageSecret; + } + + public String getEndPoint() { + return endPoint; + } + + public String getRegion() { + return region; + } + + public AuthType getAuthType() { + return authType; + } + + public static Builder builder(StorageType type) { + return new Builder(type); + } + + public static class Builder { + private final StorageType type; + private String storageKey = ""; + private String storageSecret = ""; + private String endPoint; + private String region; + private AuthType authType = AuthType.ACCESS_KEY; + + private Builder(StorageType type) { + this.type = type; + } + + public Builder storageKey(String storageKey) { + this.storageKey = storageKey; + return this; + } + + public Builder storageSecret(String storageSecret) { + this.storageSecret = storageSecret; + return this; + } + + public Builder endPoint(String endPoint) { + this.endPoint = endPoint; + return this; + } + + public Builder region(String region) { + this.region = region; + return this; + } + + public Builder authType(AuthType authType) { + this.authType = authType; + return this; + } + + public StorageConfig build() { + return new StorageConfig(this); + } + } +} diff --git a/cloud-storage-sdk-api/src/main/java/org/sunbird/cloud/storage/StorageServiceFactory.java b/cloud-storage-sdk-api/src/main/java/org/sunbird/cloud/storage/StorageServiceFactory.java new file mode 100644 index 0000000..e943499 --- /dev/null +++ b/cloud-storage-sdk-api/src/main/java/org/sunbird/cloud/storage/StorageServiceFactory.java @@ -0,0 +1,39 @@ +package org.sunbird.cloud.storage; + +import org.sunbird.cloud.storage.exception.StorageServiceException; + +import java.util.ServiceLoader; + +/** + * Factory for creating {@link IStorageService} instances. + * Uses Java {@link ServiceLoader} to discover CSP-specific implementations at runtime. + * + *

Consumers add the appropriate CSP module JAR to their classpath and the factory + * automatically discovers and uses the matching provider.

+ */ +public final class StorageServiceFactory { + + private StorageServiceFactory() { + } + + /** + * Create a storage service for the given configuration. + * The appropriate provider is discovered via ServiceLoader based on the storage type. + * + * @param config the storage configuration + * @return a new storage service instance + * @throws StorageServiceException if no provider is found for the given type + */ + public static IStorageService getStorageService(StorageConfig config) { + ServiceLoader loader = + ServiceLoader.load(StorageServiceProvider.class); + for (StorageServiceProvider provider : loader) { + if (provider.supports(config.getType())) { + return provider.create(config); + } + } + throw new StorageServiceException( + "No StorageService provider found for type: " + config.getType() + + ". Ensure the appropriate CSP module JAR is on the classpath."); + } +} diff --git a/cloud-storage-sdk-api/src/main/java/org/sunbird/cloud/storage/StorageServiceProvider.java b/cloud-storage-sdk-api/src/main/java/org/sunbird/cloud/storage/StorageServiceProvider.java new file mode 100644 index 0000000..e91b66e --- /dev/null +++ b/cloud-storage-sdk-api/src/main/java/org/sunbird/cloud/storage/StorageServiceProvider.java @@ -0,0 +1,25 @@ +package org.sunbird.cloud.storage; + +/** + * Service Provider Interface for cloud storage implementations. + * Each CSP module provides an implementation registered via + * {@code META-INF/services/org.sunbird.cloud.storage.StorageServiceProvider}. + */ +public interface StorageServiceProvider { + + /** + * Check whether this provider supports the given storage type. + * + * @param type the storage type + * @return true if this provider can create a service for the given type + */ + boolean supports(StorageConfig.StorageType type); + + /** + * Create a new storage service instance for the given configuration. + * + * @param config the storage configuration + * @return a new storage service instance + */ + IStorageService create(StorageConfig config); +} diff --git a/cloud-storage-sdk-api/src/main/java/org/sunbird/cloud/storage/exception/StorageServiceException.java b/cloud-storage-sdk-api/src/main/java/org/sunbird/cloud/storage/exception/StorageServiceException.java new file mode 100644 index 0000000..75ff16d --- /dev/null +++ b/cloud-storage-sdk-api/src/main/java/org/sunbird/cloud/storage/exception/StorageServiceException.java @@ -0,0 +1,15 @@ +package org.sunbird.cloud.storage.exception; + +/** + * Runtime exception for cloud storage operations. + */ +public class StorageServiceException extends RuntimeException { + + public StorageServiceException(String message) { + super(message); + } + + public StorageServiceException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/cloud-storage-sdk-api/src/main/java/org/sunbird/cloud/storage/model/Blob.java b/cloud-storage-sdk-api/src/main/java/org/sunbird/cloud/storage/model/Blob.java new file mode 100644 index 0000000..e8b5af0 --- /dev/null +++ b/cloud-storage-sdk-api/src/main/java/org/sunbird/cloud/storage/model/Blob.java @@ -0,0 +1,56 @@ +package org.sunbird.cloud.storage.model; + +import java.util.Collections; +import java.util.Date; +import java.util.Map; + +/** + * Represents a cloud storage blob object with metadata and optional payload. + */ +public final class Blob { + + private final String key; + private final long contentLength; + private final Date lastModified; + private final Map metadata; + private final byte[] payload; + + public Blob(String key, long contentLength, Date lastModified, + Map metadata, byte[] payload) { + this.key = key; + this.contentLength = contentLength; + this.lastModified = lastModified; + this.metadata = metadata != null ? Collections.unmodifiableMap(metadata) : Collections.emptyMap(); + this.payload = payload; + } + + public Blob(String key, long contentLength, Date lastModified, Map metadata) { + this(key, contentLength, lastModified, metadata, null); + } + + public String getKey() { + return key; + } + + public long getContentLength() { + return contentLength; + } + + public Date getLastModified() { + return lastModified; + } + + public Map getMetadata() { + return metadata; + } + + public byte[] getPayload() { + return payload; + } + + @Override + public String toString() { + return "Blob{key='" + key + "', contentLength=" + contentLength + + ", lastModified=" + lastModified + "}"; + } +} diff --git a/cloud-storage-sdk-api/src/main/java/org/sunbird/cloud/storage/model/DeleteTarget.java b/cloud-storage-sdk-api/src/main/java/org/sunbird/cloud/storage/model/DeleteTarget.java new file mode 100644 index 0000000..496b318 --- /dev/null +++ b/cloud-storage-sdk-api/src/main/java/org/sunbird/cloud/storage/model/DeleteTarget.java @@ -0,0 +1,36 @@ +package org.sunbird.cloud.storage.model; + +/** + * Represents a target for deletion in cloud storage, combining object key with directory flag. + */ +public final class DeleteTarget { + + private final String objectKey; + private final boolean directory; + + public DeleteTarget(String objectKey, boolean directory) { + this.objectKey = objectKey; + this.directory = directory; + } + + public static DeleteTarget file(String objectKey) { + return new DeleteTarget(objectKey, false); + } + + public static DeleteTarget directory(String objectKey) { + return new DeleteTarget(objectKey, true); + } + + public String getObjectKey() { + return objectKey; + } + + public boolean isDirectory() { + return directory; + } + + @Override + public String toString() { + return "DeleteTarget{key='" + objectKey + "', directory=" + directory + "}"; + } +} diff --git a/cloud-storage-sdk-api/src/main/java/org/sunbird/cloud/storage/util/DateRangeUtil.java b/cloud-storage-sdk-api/src/main/java/org/sunbird/cloud/storage/util/DateRangeUtil.java new file mode 100644 index 0000000..37be05f --- /dev/null +++ b/cloud-storage-sdk-api/src/main/java/org/sunbird/cloud/storage/util/DateRangeUtil.java @@ -0,0 +1,56 @@ +package org.sunbird.cloud.storage.util; + +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.time.temporal.ChronoUnit; +import java.util.ArrayList; +import java.util.List; + +/** + * Date range utility for computing date-based prefixes used in object search operations. + * Replaces Joda-Time usage with java.time API. + */ +public final class DateRangeUtil { + + private static final DateTimeFormatter DEFAULT_FORMAT = + DateTimeFormatter.ofPattern("yyyy-MM-dd"); + + private DateRangeUtil() { + } + + /** + * Calculate the start date by subtracting delta days from the end date. + * + * @param endDate end date string (nullable — defaults to today) + * @param delta number of days to subtract + * @return the start date as a string in yyyy-MM-dd format + */ + public static String getStartDate(String endDate, int delta) { + LocalDate to = (endDate != null && !endDate.isEmpty()) + ? LocalDate.parse(endDate, DEFAULT_FORMAT) + : LocalDate.now(); + return to.minusDays(delta).toString(); + } + + /** + * Get all dates between fromDate and toDate (inclusive) formatted with the given pattern. + * + * @param fromDate start date string + * @param toDate end date string (nullable — defaults to today) + * @param pattern date format pattern + * @return array of formatted date strings + */ + public static String[] getDatesBetween(String fromDate, String toDate, String pattern) { + DateTimeFormatter df = DateTimeFormatter.ofPattern(pattern); + LocalDate to = (toDate != null && !toDate.isEmpty()) + ? LocalDate.parse(toDate, df) + : LocalDate.now(); + LocalDate from = LocalDate.parse(fromDate, df); + long daysBetween = ChronoUnit.DAYS.between(from, to); + List dates = new ArrayList<>(); + for (long i = 0; i <= daysBetween; i++) { + dates.add(from.plusDays(i).format(df)); + } + return dates.toArray(new String[0]); + } +} diff --git a/cloud-storage-sdk-api/src/main/java/org/sunbird/cloud/storage/util/FileUtil.java b/cloud-storage-sdk-api/src/main/java/org/sunbird/cloud/storage/util/FileUtil.java new file mode 100644 index 0000000..618e92a --- /dev/null +++ b/cloud-storage-sdk-api/src/main/java/org/sunbird/cloud/storage/util/FileUtil.java @@ -0,0 +1,102 @@ +package org.sunbird.cloud.storage.util; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.nio.file.StandardCopyOption; +import java.util.ArrayList; +import java.util.List; +import java.util.zip.ZipEntry; +import java.util.zip.ZipInputStream; + +/** + * File utility methods for recursive listing, stream copy, and zip extraction. + */ +public final class FileUtil { + + private FileUtil() { + } + + /** + * Recursively list all files under the given directory. + * + * @param file the directory or file to list + * @return list of files (not directories) + */ + public static List listFilesRecursive(File file) { + List result = new ArrayList<>(); + if (file.exists() && file.isDirectory()) { + File[] children = file.listFiles(); + if (children != null) { + for (File child : children) { + if (child.isDirectory()) { + result.addAll(listFilesRecursive(child)); + } else { + result.add(child); + } + } + } + } else if (file.exists() && file.isFile()) { + result.add(file); + } + return result; + } + + /** + * Copy an input stream to a file at the given path. + * + * @param from the source input stream + * @param path the destination directory + * @param fileName the destination file name + * @throws IOException if an I/O error occurs + */ + public static void copyStream(InputStream from, String path, String fileName) throws IOException { + Files.createDirectories(Paths.get(path)); + Files.copy(from, Paths.get(path, fileName), StandardCopyOption.REPLACE_EXISTING); + } + + /** + * Extract a ZIP archive to the specified output folder. + * Skips hidden files (starting with '.'), underscore-prefixed files, and blank file names. + * + * @param zipFile path to the ZIP file + * @param outputFolder path to the output directory + * @throws IOException if an I/O error occurs + */ + public static void unZip(String zipFile, String outputFolder) throws IOException { + File folder = new File(outputFolder); + if (!folder.exists()) { + folder.mkdirs(); + } + + byte[] buffer = new byte[4096]; + try (ZipInputStream zis = new ZipInputStream(new FileInputStream(zipFile))) { + ZipEntry entry; + while ((entry = zis.getNextEntry()) != null) { + String entryName = entry.getName(); + String simpleName = Paths.get(entryName).getFileName().toString(); + + if (simpleName.startsWith(".") || simpleName.startsWith("_") || simpleName.isEmpty()) { + continue; + } + + File newFile = new File(outputFolder + File.separator + entryName); + File parent = newFile.getParentFile(); + if (parent != null && !parent.exists()) { + parent.mkdirs(); + } + + try (FileOutputStream fos = new FileOutputStream(newFile)) { + int len; + while ((len = zis.read(buffer)) > 0) { + fos.write(buffer, 0, len); + } + } + } + } + } +} diff --git a/cloud-storage-sdk-aws/pom.xml b/cloud-storage-sdk-aws/pom.xml new file mode 100644 index 0000000..803c473 --- /dev/null +++ b/cloud-storage-sdk-aws/pom.xml @@ -0,0 +1,40 @@ + + 4.0.0 + + org.sunbird + cloud-storage-sdk-parent + 2.0.0 + + + cloud-storage-sdk-aws + jar + Cloud Storage SDK - AWS + AWS S3 and Ceph S3-compatible storage implementation using AWS SDK v2. + + + + org.sunbird + cloud-storage-sdk-api + + + software.amazon.awssdk + s3 + + + software.amazon.awssdk + sts + + + org.junit.jupiter + junit-jupiter + test + + + org.mockito + mockito-core + test + + + diff --git a/cloud-storage-sdk-aws/src/main/java/org/sunbird/cloud/storage/service/aws/AwsStorageService.java b/cloud-storage-sdk-aws/src/main/java/org/sunbird/cloud/storage/service/aws/AwsStorageService.java new file mode 100644 index 0000000..e232e01 --- /dev/null +++ b/cloud-storage-sdk-aws/src/main/java/org/sunbird/cloud/storage/service/aws/AwsStorageService.java @@ -0,0 +1,372 @@ +package org.sunbird.cloud.storage.service.aws; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.sunbird.cloud.storage.AbstractStorageService; +import org.sunbird.cloud.storage.StorageConfig; +import org.sunbird.cloud.storage.exception.StorageServiceException; +import software.amazon.awssdk.auth.credentials.AwsBasicCredentials; +import software.amazon.awssdk.auth.credentials.DefaultCredentialsProvider; +import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider; +import software.amazon.awssdk.core.sync.RequestBody; +import software.amazon.awssdk.regions.Region; +import software.amazon.awssdk.services.s3.S3Client; +import software.amazon.awssdk.services.s3.S3ClientBuilder; +import software.amazon.awssdk.services.s3.S3Configuration; +import software.amazon.awssdk.services.s3.model.BucketAlreadyOwnedByYouException; +import software.amazon.awssdk.services.s3.model.CopyObjectRequest; +import software.amazon.awssdk.services.s3.model.CreateBucketRequest; +import software.amazon.awssdk.services.s3.model.Delete; +import software.amazon.awssdk.services.s3.model.DeleteObjectsRequest; +import software.amazon.awssdk.services.s3.model.GetObjectRequest; +import software.amazon.awssdk.services.s3.model.HeadBucketRequest; +import software.amazon.awssdk.services.s3.model.HeadObjectRequest; +import software.amazon.awssdk.services.s3.model.HeadObjectResponse; +import software.amazon.awssdk.services.s3.model.ListObjectsV2Request; +import software.amazon.awssdk.services.s3.model.NoSuchBucketException; +import software.amazon.awssdk.services.s3.model.ObjectIdentifier; +import software.amazon.awssdk.services.s3.model.PutObjectRequest; +import software.amazon.awssdk.services.s3.model.S3Object; +import software.amazon.awssdk.services.s3.paginators.ListObjectsV2Iterable; +import software.amazon.awssdk.services.s3.presigner.S3Presigner; +import software.amazon.awssdk.services.s3.presigner.model.GetObjectPresignRequest; +import software.amazon.awssdk.services.s3.presigner.model.PutObjectPresignRequest; + +import java.io.File; +import java.io.InputStream; +import java.net.URI; +import java.time.Duration; +import java.util.ArrayList; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +/** + * AWS S3 storage service implementation using AWS SDK v2. + * + *

Supports standard AWS S3, Ceph S3-compatible, and OCI S3-compatible storage + * endpoints. Authentication can be configured via access keys, OIDC, IAM roles, + * or instance profiles.

+ */ +public class AwsStorageService extends AbstractStorageService { + + private static final Logger logger = LoggerFactory.getLogger(AwsStorageService.class); + + private final StorageConfig config; + private final S3Client s3Client; + private final S3Presigner s3Presigner; + + public AwsStorageService(StorageConfig config) { + this.config = config; + + Region region = (config.getRegion() != null && !config.getRegion().isEmpty()) + ? Region.of(config.getRegion()) + : Region.US_EAST_1; + + boolean useCustomEndpoint = isCustomEndpoint(config); + String endPoint = config.getEndPoint(); + + // Build S3Client + S3ClientBuilder clientBuilder = S3Client.builder().region(region); + S3Presigner.Builder presignerBuilder = S3Presigner.builder().region(region); + + // Configure credentials based on auth type + switch (config.getAuthType()) { + case ACCESS_KEY: + StaticCredentialsProvider staticCreds = StaticCredentialsProvider.create( + AwsBasicCredentials.create(config.getStorageKey(), config.getStorageSecret())); + clientBuilder.credentialsProvider(staticCreds); + presignerBuilder.credentialsProvider(staticCreds); + break; + case OIDC: + case IAM: + case IAM_ROLE: + case INSTANCE_PROFILE: + DefaultCredentialsProvider defaultCreds = DefaultCredentialsProvider.create(); + clientBuilder.credentialsProvider(defaultCreds); + presignerBuilder.credentialsProvider(defaultCreds); + break; + default: + throw new StorageServiceException("Unsupported auth type: " + config.getAuthType()); + } + + // Configure custom endpoint and path-style access for non-AWS S3 implementations + if (useCustomEndpoint && endPoint != null && !endPoint.isEmpty()) { + URI endpointUri = URI.create(endPoint); + clientBuilder.endpointOverride(endpointUri); + presignerBuilder.endpointOverride(endpointUri); + + S3Configuration s3Config = S3Configuration.builder() + .pathStyleAccessEnabled(true) + .build(); + clientBuilder.serviceConfiguration(s3Config); + } + + this.s3Client = clientBuilder.build(); + this.s3Presigner = presignerBuilder.build(); + + logger.info("Initialized AwsStorageService for type={}, region={}, customEndpoint={}", + config.getType(), region, useCustomEndpoint); + } + + /** + * Determines whether a custom endpoint should be configured. + * Custom endpoints are used for CEPHS3, OCI, or when an explicit endpoint is set. + */ + private boolean isCustomEndpoint(StorageConfig config) { + if (config.getType() == StorageConfig.StorageType.CEPHS3 + || config.getType() == StorageConfig.StorageType.OCI) { + return true; + } + return config.getEndPoint() != null && !config.getEndPoint().isEmpty(); + } + + @Override + protected void ensureContainerExists(String container) { + try { + s3Client.headBucket(HeadBucketRequest.builder().bucket(container).build()); + } catch (NoSuchBucketException e) { + try { + s3Client.createBucket(CreateBucketRequest.builder().bucket(container).build()); + logger.info("Created bucket: {}", container); + } catch (BucketAlreadyOwnedByYouException ignored) { + // Bucket was created concurrently; safe to proceed + logger.debug("Bucket already owned by us: {}", container); + } catch (Exception ex) { + throw new StorageServiceException( + "Failed to create bucket: " + container + " - " + ex.getMessage(), ex); + } + } catch (Exception e) { + throw new StorageServiceException( + "Failed to check bucket existence: " + container + " - " + e.getMessage(), e); + } + } + + @Override + protected String putObject(String container, String objectKey, File file) { + try { + String contentType = tika.detect(file); + PutObjectRequest request = PutObjectRequest.builder() + .bucket(container) + .key(objectKey) + .contentType(contentType) + .build(); + s3Client.putObject(request, RequestBody.fromFile(file)); + return getObjectUri(container, objectKey); + } catch (Exception e) { + throw new StorageServiceException( + "Failed to put object from file: " + objectKey + " - " + e.getMessage(), e); + } + } + + @Override + protected String putObject(String container, String objectKey, byte[] content) { + try { + PutObjectRequest request = PutObjectRequest.builder() + .bucket(container) + .key(objectKey) + .contentType("application/octet-stream") + .build(); + s3Client.putObject(request, RequestBody.fromBytes(content)); + return getObjectUri(container, objectKey); + } catch (Exception e) { + throw new StorageServiceException( + "Failed to put object from bytes: " + objectKey + " - " + e.getMessage(), e); + } + } + + @Override + protected InputStream getObjectStream(String container, String objectKey) { + try { + GetObjectRequest request = GetObjectRequest.builder() + .bucket(container) + .key(objectKey) + .build(); + return s3Client.getObject(request); + } catch (Exception e) { + throw new StorageServiceException( + "Failed to get object stream: " + objectKey + " - " + e.getMessage(), e); + } + } + + @Override + protected String getObjectUri(String container, String objectKey) { + String endPoint = config.getEndPoint(); + if (endPoint != null && !endPoint.isEmpty()) { + // Custom endpoint: // + String base = endPoint.endsWith("/") ? endPoint.substring(0, endPoint.length() - 1) : endPoint; + return base + "/" + container + "/" + objectKey; + } + // Standard AWS S3: https://.s3.amazonaws.com/ + return "https://" + container + ".s3.amazonaws.com/" + objectKey; + } + + @Override + protected BlobDetail getObjectDetail(String container, String objectKey) { + try { + HeadObjectRequest request = HeadObjectRequest.builder() + .bucket(container) + .key(objectKey) + .build(); + HeadObjectResponse response = s3Client.headObject(request); + + long contentLength = response.contentLength() != null ? response.contentLength() : 0L; + Date lastModified = response.lastModified() != null + ? Date.from(response.lastModified()) + : null; + + Map metadata = new HashMap<>(); + if (response.metadata() != null) { + metadata.putAll(response.metadata()); + } + if (response.contentType() != null) { + metadata.put("Content-Type", response.contentType()); + } + if (response.eTag() != null) { + metadata.put("ETag", response.eTag()); + } + + String uri = getObjectUri(container, objectKey); + return new BlobDetail(objectKey, contentLength, lastModified, metadata, uri); + } catch (Exception e) { + throw new StorageServiceException( + "Failed to get object detail: " + objectKey + " - " + e.getMessage(), e); + } + } + + @Override + protected List listKeys(String container, String prefix) { + try { + ListObjectsV2Request request = ListObjectsV2Request.builder() + .bucket(container) + .prefix(prefix) + .build(); + ListObjectsV2Iterable pages = s3Client.listObjectsV2Paginator(request); + + List keys = new ArrayList<>(); + for (var page : pages) { + for (S3Object s3Object : page.contents()) { + keys.add(s3Object.key()); + } + } + return keys; + } catch (Exception e) { + throw new StorageServiceException( + "Failed to list keys with prefix: " + prefix + " - " + e.getMessage(), e); + } + } + + @Override + protected void removeObjects(String container, List objectKeys) { + try { + if (objectKeys == null || objectKeys.isEmpty()) { + return; + } + + // S3 DeleteObjects supports up to 1000 keys per request + int batchSize = 1000; + for (int i = 0; i < objectKeys.size(); i += batchSize) { + List batch = objectKeys.subList(i, Math.min(i + batchSize, objectKeys.size())); + List identifiers = batch.stream() + .map(key -> ObjectIdentifier.builder().key(key).build()) + .collect(Collectors.toList()); + + DeleteObjectsRequest request = DeleteObjectsRequest.builder() + .bucket(container) + .delete(Delete.builder().objects(identifiers).quiet(true).build()) + .build(); + s3Client.deleteObjects(request); + } + } catch (Exception e) { + throw new StorageServiceException( + "Failed to remove objects from container: " + container + " - " + e.getMessage(), e); + } + } + + @Override + protected void copyObject(String fromContainer, String fromKey, + String toContainer, String toKey) { + try { + CopyObjectRequest request = CopyObjectRequest.builder() + .sourceBucket(fromContainer) + .sourceKey(fromKey) + .destinationBucket(toContainer) + .destinationKey(toKey) + .build(); + s3Client.copyObject(request); + } catch (Exception e) { + throw new StorageServiceException( + "Failed to copy object from " + fromContainer + "/" + fromKey + + " to " + toContainer + "/" + toKey + " - " + e.getMessage(), e); + } + } + + @Override + protected String generateSignedGetUrl(String container, String objectKey, int ttlSeconds) { + try { + GetObjectRequest getObjectRequest = GetObjectRequest.builder() + .bucket(container) + .key(objectKey) + .build(); + GetObjectPresignRequest presignRequest = GetObjectPresignRequest.builder() + .signatureDuration(Duration.ofSeconds(ttlSeconds)) + .getObjectRequest(getObjectRequest) + .build(); + return s3Presigner.presignGetObject(presignRequest).url().toString(); + } catch (Exception e) { + throw new StorageServiceException( + "Failed to generate signed GET URL for: " + objectKey + " - " + e.getMessage(), e); + } + } + + @Override + protected String generateSignedPutUrl(String container, String objectKey, + int ttlSeconds, String contentType, + Map additionalParams) { + try { + PutObjectRequest.Builder putRequestBuilder = PutObjectRequest.builder() + .bucket(container) + .key(objectKey); + if (contentType != null && !contentType.isEmpty()) { + putRequestBuilder.contentType(contentType); + } + if (additionalParams != null && !additionalParams.isEmpty()) { + putRequestBuilder.metadata(additionalParams); + } + + PutObjectPresignRequest presignRequest = PutObjectPresignRequest.builder() + .signatureDuration(Duration.ofSeconds(ttlSeconds)) + .putObjectRequest(putRequestBuilder.build()) + .build(); + return s3Presigner.presignPutObject(presignRequest).url().toString(); + } catch (Exception e) { + throw new StorageServiceException( + "Failed to generate signed PUT URL for: " + objectKey + " - " + e.getMessage(), e); + } + } + + @Override + protected String getHdfsPrefix(String container) { + return "s3n://" + container + "/"; + } + + @Override + public void close() { + try { + if (s3Client != null) { + s3Client.close(); + } + } catch (Exception e) { + logger.warn("Error closing S3Client", e); + } + try { + if (s3Presigner != null) { + s3Presigner.close(); + } + } catch (Exception e) { + logger.warn("Error closing S3Presigner", e); + } + } +} diff --git a/cloud-storage-sdk-aws/src/main/java/org/sunbird/cloud/storage/service/aws/AwsStorageServiceProvider.java b/cloud-storage-sdk-aws/src/main/java/org/sunbird/cloud/storage/service/aws/AwsStorageServiceProvider.java new file mode 100644 index 0000000..214f8b0 --- /dev/null +++ b/cloud-storage-sdk-aws/src/main/java/org/sunbird/cloud/storage/service/aws/AwsStorageServiceProvider.java @@ -0,0 +1,22 @@ +package org.sunbird.cloud.storage.service.aws; + +import org.sunbird.cloud.storage.IStorageService; +import org.sunbird.cloud.storage.StorageConfig; +import org.sunbird.cloud.storage.StorageServiceProvider; + +/** + * SPI provider for AWS S3 and Ceph S3-compatible storage. + */ +public class AwsStorageServiceProvider implements StorageServiceProvider { + + @Override + public boolean supports(StorageConfig.StorageType type) { + return type == StorageConfig.StorageType.AWS + || type == StorageConfig.StorageType.CEPHS3; + } + + @Override + public IStorageService create(StorageConfig config) { + return new AwsStorageService(config); + } +} diff --git a/cloud-storage-sdk-aws/src/main/resources/META-INF/services/org.sunbird.cloud.storage.StorageServiceProvider b/cloud-storage-sdk-aws/src/main/resources/META-INF/services/org.sunbird.cloud.storage.StorageServiceProvider new file mode 100644 index 0000000..1a8c547 --- /dev/null +++ b/cloud-storage-sdk-aws/src/main/resources/META-INF/services/org.sunbird.cloud.storage.StorageServiceProvider @@ -0,0 +1 @@ +org.sunbird.cloud.storage.service.aws.AwsStorageServiceProvider diff --git a/cloud-storage-sdk-azure/pom.xml b/cloud-storage-sdk-azure/pom.xml new file mode 100644 index 0000000..223dce0 --- /dev/null +++ b/cloud-storage-sdk-azure/pom.xml @@ -0,0 +1,40 @@ + + 4.0.0 + + org.sunbird + cloud-storage-sdk-parent + 2.0.0 + + + cloud-storage-sdk-azure + jar + Cloud Storage SDK - Azure + Azure Blob Storage implementation using Azure SDK for Java. + + + + org.sunbird + cloud-storage-sdk-api + + + com.azure + azure-storage-blob + + + com.azure + azure-identity + + + org.junit.jupiter + junit-jupiter + test + + + org.mockito + mockito-core + test + + + diff --git a/cloud-storage-sdk-azure/src/main/java/org/sunbird/cloud/storage/service/azure/AzureStorageService.java b/cloud-storage-sdk-azure/src/main/java/org/sunbird/cloud/storage/service/azure/AzureStorageService.java new file mode 100644 index 0000000..3838f9c --- /dev/null +++ b/cloud-storage-sdk-azure/src/main/java/org/sunbird/cloud/storage/service/azure/AzureStorageService.java @@ -0,0 +1,289 @@ +package org.sunbird.cloud.storage.service.azure; + +import com.azure.identity.DefaultAzureCredentialBuilder; +import com.azure.identity.ManagedIdentityCredentialBuilder; +import com.azure.identity.WorkloadIdentityCredentialBuilder; +import com.azure.storage.blob.BlobClient; +import com.azure.storage.blob.BlobContainerClient; +import com.azure.storage.blob.BlobServiceClient; +import com.azure.storage.blob.BlobServiceClientBuilder; +import com.azure.storage.blob.models.BlobItem; +import com.azure.storage.blob.models.BlobListDetails; +import com.azure.storage.blob.models.BlobProperties; +import com.azure.storage.blob.models.ListBlobsOptions; +import com.azure.storage.blob.sas.BlobSasPermission; +import com.azure.storage.blob.sas.BlobServiceSasSignatureValues; +import com.azure.storage.common.StorageSharedKeyCredential; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.sunbird.cloud.storage.AbstractStorageService; +import org.sunbird.cloud.storage.StorageConfig; +import org.sunbird.cloud.storage.exception.StorageServiceException; + +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.InputStream; +import java.time.OffsetDateTime; +import java.util.ArrayList; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Azure Blob Storage implementation using Azure SDK for Java. + */ +public class AzureStorageService extends AbstractStorageService { + + private static final Logger logger = LoggerFactory.getLogger(AzureStorageService.class); + + private final StorageConfig config; + private final BlobServiceClient blobServiceClient; + private final boolean useSharedKey; + + public AzureStorageService(StorageConfig config) { + this.config = config; + + String accountName = config.getStorageKey(); + String endpoint = "https://" + accountName + ".blob.core.windows.net"; + + BlobServiceClientBuilder builder = new BlobServiceClientBuilder().endpoint(endpoint); + + switch (config.getAuthType()) { + case ACCESS_KEY: + StorageSharedKeyCredential sharedKey = + new StorageSharedKeyCredential(accountName, config.getStorageSecret()); + builder.credential(sharedKey); + this.useSharedKey = true; + break; + case OIDC: + String clientId = System.getenv("AZURE_CLIENT_ID"); + String tenantId = System.getenv("AZURE_TENANT_ID"); + String tokenFile = System.getenv("AZURE_FEDERATED_TOKEN_FILE"); + if (clientId != null && tenantId != null && tokenFile != null) { + builder.credential(new WorkloadIdentityCredentialBuilder() + .clientId(clientId) + .tenantId(tenantId) + .tokenFilePath(tokenFile) + .build()); + } else { + builder.credential(new DefaultAzureCredentialBuilder().build()); + } + this.useSharedKey = false; + break; + case IAM: + case IAM_ROLE: + case INSTANCE_PROFILE: + builder.credential(new ManagedIdentityCredentialBuilder().build()); + this.useSharedKey = false; + break; + default: + throw new StorageServiceException("Unsupported auth type: " + config.getAuthType()); + } + + this.blobServiceClient = builder.buildClient(); + logger.info("Initialized AzureStorageService for account={}, authType={}", + accountName, config.getAuthType()); + } + + private BlobContainerClient getContainerClient(String container) { + return blobServiceClient.getBlobContainerClient(container); + } + + private BlobClient getBlobClient(String container, String objectKey) { + return getContainerClient(container).getBlobClient(objectKey); + } + + @Override + protected void ensureContainerExists(String container) { + try { + BlobContainerClient containerClient = getContainerClient(container); + if (!containerClient.exists()) { + containerClient.create(); + logger.info("Created container: {}", container); + } + } catch (Exception e) { + throw new StorageServiceException( + "Failed to ensure container exists: " + container + " - " + e.getMessage(), e); + } + } + + @Override + protected String putObject(String container, String objectKey, File file) { + try { + BlobClient blobClient = getBlobClient(container, objectKey); + String contentType = tika.detect(file); + blobClient.uploadFromFile(file.getAbsolutePath(), true); + blobClient.setHttpHeaders(new com.azure.storage.blob.models.BlobHttpHeaders() + .setContentType(contentType)); + return blobClient.getBlobUrl(); + } catch (Exception e) { + throw new StorageServiceException( + "Failed to put object from file: " + objectKey + " - " + e.getMessage(), e); + } + } + + @Override + protected String putObject(String container, String objectKey, byte[] content) { + try { + BlobClient blobClient = getBlobClient(container, objectKey); + blobClient.upload(new ByteArrayInputStream(content), content.length, true); + return blobClient.getBlobUrl(); + } catch (Exception e) { + throw new StorageServiceException( + "Failed to put object from bytes: " + objectKey + " - " + e.getMessage(), e); + } + } + + @Override + protected InputStream getObjectStream(String container, String objectKey) { + try { + BlobClient blobClient = getBlobClient(container, objectKey); + return blobClient.openInputStream(); + } catch (Exception e) { + throw new StorageServiceException( + "Failed to get object stream: " + objectKey + " - " + e.getMessage(), e); + } + } + + @Override + protected String getObjectUri(String container, String objectKey) { + return getBlobClient(container, objectKey).getBlobUrl(); + } + + @Override + protected BlobDetail getObjectDetail(String container, String objectKey) { + try { + BlobClient blobClient = getBlobClient(container, objectKey); + BlobProperties props = blobClient.getProperties(); + + Map metadata = new HashMap<>(); + if (props.getMetadata() != null) { + metadata.putAll(props.getMetadata()); + } + metadata.put("uri", blobClient.getBlobUrl()); + metadata.put("Content-Type", props.getContentType()); + metadata.put("ETag", props.getETag()); + + Date lastModified = props.getLastModified() != null + ? Date.from(props.getLastModified().toInstant()) + : null; + + return new BlobDetail(objectKey, props.getBlobSize(), lastModified, + metadata, blobClient.getBlobUrl()); + } catch (Exception e) { + throw new StorageServiceException( + "Failed to get object detail: " + objectKey + " - " + e.getMessage(), e); + } + } + + @Override + protected List listKeys(String container, String prefix) { + try { + BlobContainerClient containerClient = getContainerClient(container); + ListBlobsOptions options = new ListBlobsOptions() + .setPrefix(prefix) + .setDetails(new BlobListDetails().setRetrieveMetadata(false)); + + List keys = new ArrayList<>(); + for (BlobItem item : containerClient.listBlobs(options, null)) { + keys.add(item.getName()); + } + return keys; + } catch (Exception e) { + throw new StorageServiceException( + "Failed to list keys with prefix: " + prefix + " - " + e.getMessage(), e); + } + } + + @Override + protected void removeObjects(String container, List objectKeys) { + try { + for (String key : objectKeys) { + BlobClient blobClient = getBlobClient(container, key); + blobClient.deleteIfExists(); + } + } catch (Exception e) { + throw new StorageServiceException( + "Failed to remove objects: " + e.getMessage(), e); + } + } + + @Override + protected void copyObject(String fromContainer, String fromKey, + String toContainer, String toKey) { + try { + BlobClient sourceBlobClient = getBlobClient(fromContainer, fromKey); + BlobClient destBlobClient = getBlobClient(toContainer, toKey); + destBlobClient.copyFromUrl(sourceBlobClient.getBlobUrl()); + } catch (Exception e) { + throw new StorageServiceException( + "Failed to copy object from " + fromContainer + "/" + fromKey + + " to " + toContainer + "/" + toKey + " - " + e.getMessage(), e); + } + } + + @Override + protected String generateSignedGetUrl(String container, String objectKey, int ttlSeconds) { + try { + BlobClient blobClient = getBlobClient(container, objectKey); + BlobSasPermission permission = new BlobSasPermission().setReadPermission(true); + BlobServiceSasSignatureValues values = new BlobServiceSasSignatureValues( + OffsetDateTime.now().plusSeconds(ttlSeconds), permission); + + if (useSharedKey) { + return blobClient.getBlobUrl() + "?" + blobClient.generateSas(values); + } else { + var delegationKey = blobServiceClient.getUserDelegationKey( + OffsetDateTime.now().minusMinutes(5), + OffsetDateTime.now().plusSeconds(ttlSeconds)); + return blobClient.getBlobUrl() + "?" + + blobClient.generateUserDelegationSas(values, delegationKey); + } + } catch (Exception e) { + throw new StorageServiceException( + "Failed to generate signed GET URL for: " + objectKey + " - " + e.getMessage(), e); + } + } + + @Override + protected String generateSignedPutUrl(String container, String objectKey, + int ttlSeconds, String contentType, + Map additionalParams) { + try { + BlobClient blobClient = getBlobClient(container, objectKey); + BlobSasPermission permission = new BlobSasPermission() + .setWritePermission(true) + .setCreatePermission(true); + BlobServiceSasSignatureValues values = new BlobServiceSasSignatureValues( + OffsetDateTime.now().plusSeconds(ttlSeconds), permission); + if (contentType != null) { + values.setContentType(contentType); + } + + if (useSharedKey) { + return blobClient.getBlobUrl() + "?" + blobClient.generateSas(values); + } else { + var delegationKey = blobServiceClient.getUserDelegationKey( + OffsetDateTime.now().minusMinutes(5), + OffsetDateTime.now().plusSeconds(ttlSeconds)); + return blobClient.getBlobUrl() + "?" + + blobClient.generateUserDelegationSas(values, delegationKey); + } + } catch (Exception e) { + throw new StorageServiceException( + "Failed to generate signed PUT URL for: " + objectKey + " - " + e.getMessage(), e); + } + } + + @Override + protected String getHdfsPrefix(String container) { + return "wasb://" + container + "@" + config.getStorageKey() + ".blob.core.windows.net/"; + } + + @Override + public void close() { + // BlobServiceClient does not implement Closeable; no resource cleanup needed + logger.info("AzureStorageService closed"); + } +} diff --git a/cloud-storage-sdk-azure/src/main/java/org/sunbird/cloud/storage/service/azure/AzureStorageServiceProvider.java b/cloud-storage-sdk-azure/src/main/java/org/sunbird/cloud/storage/service/azure/AzureStorageServiceProvider.java new file mode 100644 index 0000000..5008a3c --- /dev/null +++ b/cloud-storage-sdk-azure/src/main/java/org/sunbird/cloud/storage/service/azure/AzureStorageServiceProvider.java @@ -0,0 +1,21 @@ +package org.sunbird.cloud.storage.service.azure; + +import org.sunbird.cloud.storage.IStorageService; +import org.sunbird.cloud.storage.StorageConfig; +import org.sunbird.cloud.storage.StorageServiceProvider; + +/** + * SPI provider for Azure Blob Storage. + */ +public class AzureStorageServiceProvider implements StorageServiceProvider { + + @Override + public boolean supports(StorageConfig.StorageType type) { + return type == StorageConfig.StorageType.AZURE; + } + + @Override + public IStorageService create(StorageConfig config) { + return new AzureStorageService(config); + } +} diff --git a/cloud-storage-sdk-azure/src/main/resources/META-INF/services/org.sunbird.cloud.storage.StorageServiceProvider b/cloud-storage-sdk-azure/src/main/resources/META-INF/services/org.sunbird.cloud.storage.StorageServiceProvider new file mode 100644 index 0000000..89e50d6 --- /dev/null +++ b/cloud-storage-sdk-azure/src/main/resources/META-INF/services/org.sunbird.cloud.storage.StorageServiceProvider @@ -0,0 +1 @@ +org.sunbird.cloud.storage.service.azure.AzureStorageServiceProvider diff --git a/cloud-storage-sdk-gcp/pom.xml b/cloud-storage-sdk-gcp/pom.xml new file mode 100644 index 0000000..f36566c --- /dev/null +++ b/cloud-storage-sdk-gcp/pom.xml @@ -0,0 +1,36 @@ + + 4.0.0 + + org.sunbird + cloud-storage-sdk-parent + 2.0.0 + + + cloud-storage-sdk-gcp + jar + Cloud Storage SDK - GCP + Google Cloud Storage implementation using Google Cloud Java SDK. + + + + org.sunbird + cloud-storage-sdk-api + + + com.google.cloud + google-cloud-storage + + + org.junit.jupiter + junit-jupiter + test + + + org.mockito + mockito-core + test + + + diff --git a/cloud-storage-sdk-gcp/src/main/java/org/sunbird/cloud/storage/service/gcp/GcpStorageService.java b/cloud-storage-sdk-gcp/src/main/java/org/sunbird/cloud/storage/service/gcp/GcpStorageService.java new file mode 100644 index 0000000..8ae0956 --- /dev/null +++ b/cloud-storage-sdk-gcp/src/main/java/org/sunbird/cloud/storage/service/gcp/GcpStorageService.java @@ -0,0 +1,321 @@ +package org.sunbird.cloud.storage.service.gcp; + +import com.google.auth.oauth2.GoogleCredentials; +import com.google.auth.oauth2.ServiceAccountCredentials; +import com.google.cloud.storage.BlobId; +import com.google.cloud.storage.BlobInfo; +import com.google.cloud.storage.HttpMethod; +import com.google.cloud.storage.Storage; +import com.google.cloud.storage.StorageOptions; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.sunbird.cloud.storage.AbstractStorageService; +import org.sunbird.cloud.storage.StorageConfig; +import org.sunbird.cloud.storage.exception.StorageServiceException; + +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; +import java.nio.file.Files; +import java.util.ArrayList; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +/** + * Google Cloud Storage implementation using the Google Cloud Java SDK. + */ +public class GcpStorageService extends AbstractStorageService { + + private static final Logger logger = LoggerFactory.getLogger(GcpStorageService.class); + private static final String GCS_HOST = "https://storage.googleapis.com/"; + + private final StorageConfig config; + private final Storage storage; + + public GcpStorageService(StorageConfig config) { + this.config = config; + this.storage = buildStorage(config); + logger.info("Initialized GcpStorageService with authType={}", config.getAuthType()); + } + + private Storage buildStorage(StorageConfig config) { + try { + StorageOptions.Builder builder = StorageOptions.newBuilder(); + + switch (config.getAuthType()) { + case ACCESS_KEY: + // For access key auth, storageKey is the project ID and storageSecret + // is a service account JSON key. Parse as JSON credentials. + if (config.getStorageSecret() != null && !config.getStorageSecret().isEmpty()) { + GoogleCredentials credentials = GoogleCredentials.fromStream( + new ByteArrayInputStream(config.getStorageSecret().getBytes())); + builder.setCredentials(credentials); + } + if (config.getStorageKey() != null && !config.getStorageKey().isEmpty()) { + builder.setProjectId(config.getStorageKey()); + } + break; + case OIDC: + case IAM: + case IAM_ROLE: + case INSTANCE_PROFILE: + // Use application default credentials (handles WIF, GKE Workload Identity, + // metadata server, GOOGLE_APPLICATION_CREDENTIALS) + builder.setCredentials(GoogleCredentials.getApplicationDefault()); + break; + default: + throw new StorageServiceException("Unsupported auth type: " + config.getAuthType()); + } + + return builder.build().getService(); + } catch (IOException e) { + throw new StorageServiceException("Failed to initialize GCP Storage: " + e.getMessage(), e); + } + } + + @Override + protected void ensureContainerExists(String container) { + try { + if (storage.get(container) == null) { + storage.create(com.google.cloud.storage.BucketInfo.of(container)); + logger.info("Created bucket: {}", container); + } + } catch (Exception e) { + // Bucket may already exist; log and continue + logger.debug("Bucket check/create for {}: {}", container, e.getMessage()); + } + } + + @Override + protected String putObject(String container, String objectKey, File file) { + try { + String contentType = tika.detect(file); + BlobId blobId = BlobId.of(container, objectKey); + BlobInfo blobInfo = BlobInfo.newBuilder(blobId) + .setContentType(contentType) + .build(); + storage.create(blobInfo, Files.readAllBytes(file.toPath())); + return GCS_HOST + container + "/" + objectKey; + } catch (IOException e) { + throw new StorageServiceException( + "Failed to put object from file: " + objectKey + " - " + e.getMessage(), e); + } + } + + @Override + protected String putObject(String container, String objectKey, byte[] content) { + BlobId blobId = BlobId.of(container, objectKey); + BlobInfo blobInfo = BlobInfo.newBuilder(blobId).build(); + storage.create(blobInfo, content); + return GCS_HOST + container + "/" + objectKey; + } + + @Override + protected InputStream getObjectStream(String container, String objectKey) { + try { + byte[] content = storage.readAllBytes(BlobId.of(container, objectKey)); + return new ByteArrayInputStream(content); + } catch (Exception e) { + throw new StorageServiceException( + "Failed to get object stream: " + objectKey + " - " + e.getMessage(), e); + } + } + + @Override + protected String getObjectUri(String container, String objectKey) { + return GCS_HOST + container + "/" + objectKey; + } + + @Override + protected BlobDetail getObjectDetail(String container, String objectKey) { + try { + com.google.cloud.storage.Blob blob = storage.get(BlobId.of(container, objectKey)); + if (blob == null) { + throw new StorageServiceException("Object not found: " + container + "/" + objectKey); + } + + Map metadata = new HashMap<>(); + if (blob.getMetadata() != null) { + metadata.putAll(blob.getMetadata()); + } + metadata.put("uri", GCS_HOST + container + "/" + objectKey); + metadata.put("publicUri", GCS_HOST + container + "/" + objectKey); + metadata.put("name", blob.getName()); + metadata.put("Content-Type", blob.getContentType()); + + Long size = blob.getSize(); + long contentLength = size != null ? size : 0L; + Date lastModified = blob.getUpdateTimeOffsetDateTime() != null + ? Date.from(blob.getUpdateTimeOffsetDateTime().toInstant()) + : null; + + return new BlobDetail(objectKey, contentLength, lastModified, metadata, + GCS_HOST + container + "/" + objectKey); + } catch (StorageServiceException e) { + throw e; + } catch (Exception e) { + throw new StorageServiceException( + "Failed to get object detail: " + objectKey + " - " + e.getMessage(), e); + } + } + + @Override + protected List listKeys(String container, String prefix) { + try { + List keys = new ArrayList<>(); + com.google.api.gax.paging.Page blobs = + storage.list(container, Storage.BlobListOption.prefix(prefix)); + for (com.google.cloud.storage.Blob blob : blobs.iterateAll()) { + keys.add(blob.getName()); + } + return keys; + } catch (Exception e) { + throw new StorageServiceException( + "Failed to list keys with prefix: " + prefix + " - " + e.getMessage(), e); + } + } + + @Override + protected void removeObjects(String container, List objectKeys) { + try { + List blobIds = new ArrayList<>(); + for (String key : objectKeys) { + blobIds.add(BlobId.of(container, key)); + } + if (!blobIds.isEmpty()) { + storage.delete(blobIds); + } + } catch (Exception e) { + throw new StorageServiceException( + "Failed to remove objects: " + e.getMessage(), e); + } + } + + @Override + protected void copyObject(String fromContainer, String fromKey, + String toContainer, String toKey) { + try { + Storage.CopyRequest request = Storage.CopyRequest.newBuilder() + .setSource(BlobId.of(fromContainer, fromKey)) + .setTarget(BlobId.of(toContainer, toKey)) + .build(); + storage.copy(request).getResult(); + } catch (Exception e) { + throw new StorageServiceException( + "Failed to copy object from " + fromContainer + "/" + fromKey + + " to " + toContainer + "/" + toKey + " - " + e.getMessage(), e); + } + } + + @Override + protected String generateSignedGetUrl(String container, String objectKey, int ttlSeconds) { + try { + int effectiveTtl = Math.min(ttlSeconds, maxSignedUrlTTL); + BlobInfo blobInfo = BlobInfo.newBuilder(BlobId.of(container, objectKey)).build(); + URL url = storage.signUrl(blobInfo, effectiveTtl, TimeUnit.SECONDS, + Storage.SignUrlOption.httpMethod(HttpMethod.GET), + Storage.SignUrlOption.withV4Signature()); + return url.toString(); + } catch (Exception e) { + throw new StorageServiceException( + "Failed to generate signed GET URL for: " + objectKey + " - " + e.getMessage(), e); + } + } + + @Override + protected String generateSignedPutUrl(String container, String objectKey, + int ttlSeconds, String contentType, + Map additionalParams) { + try { + int effectiveTtl = Math.min(ttlSeconds, maxSignedUrlTTL); + BlobInfo blobInfo = BlobInfo.newBuilder(BlobId.of(container, objectKey)).build(); + + // Check for chunked upload mode + boolean isChunkedUpload = additionalParams != null + && "true".equalsIgnoreCase(additionalParams.get("chunked")); + + // If additionalParams contains service account credentials, use them + Storage signingStorage = resolveSigningStorage(additionalParams); + + if (isChunkedUpload) { + Map extensionHeaders = new HashMap<>(); + extensionHeaders.put("Content-Type", + contentType != null ? contentType : "application/octet-stream"); + extensionHeaders.put("x-goog-resumable", "start"); + URL url = signingStorage.signUrl(blobInfo, effectiveTtl, TimeUnit.SECONDS, + Storage.SignUrlOption.httpMethod(HttpMethod.POST), + Storage.SignUrlOption.withV4Signature(), + Storage.SignUrlOption.withExtHeaders(extensionHeaders)); + return url.toString(); + } else { + URL url = signingStorage.signUrl(blobInfo, effectiveTtl, TimeUnit.SECONDS, + Storage.SignUrlOption.httpMethod(HttpMethod.PUT), + Storage.SignUrlOption.withV4Signature()); + return url.toString(); + } + } catch (Exception e) { + throw new StorageServiceException( + "Failed to generate signed PUT URL for: " + objectKey + " - " + e.getMessage(), e); + } + } + + /** + * If additionalParams contains GCP service account credentials, build a dedicated + * Storage client for signing. Otherwise fall back to the main storage client. + */ + private Storage resolveSigningStorage(Map additionalParams) { + if (additionalParams == null || additionalParams.isEmpty()) { + return storage; + } + String clientId = additionalParams.get("clientId"); + String clientEmail = additionalParams.get("clientEmail"); + String privateKeyPkcs8 = additionalParams.get("privateKeyPkcs8"); + String privateKeyId = additionalParams.get("privateKeyIds"); + String projectId = additionalParams.get("projectId"); + + if (clientEmail != null && privateKeyPkcs8 != null && privateKeyId != null) { + try { + ServiceAccountCredentials credentials = ServiceAccountCredentials.fromPkcs8( + clientId, clientEmail, privateKeyPkcs8, privateKeyId, + new ArrayList<>()); + return StorageOptions.newBuilder() + .setProjectId(projectId) + .setCredentials(credentials) + .build() + .getService(); + } catch (IOException e) { + throw new StorageServiceException( + "Failed to create signing credentials from additionalParams: " + e.getMessage(), e); + } + } + return storage; + } + + @Override + protected String getHdfsPrefix(String container) { + return "gs://" + container + "/"; + } + + @Override + public String getUri(String container, String prefix, boolean isDirectory) { + List keys = listObjectKeys(container, prefix); + if (keys.isEmpty()) { + throw new StorageServiceException("The given prefix is incorrect: " + prefix); + } + return GCS_HOST + container + "/" + prefix; + } + + @Override + public void close() { + try { + storage.close(); + } catch (Exception e) { + logger.warn("Error closing GCP Storage client", e); + } + } +} diff --git a/cloud-storage-sdk-gcp/src/main/java/org/sunbird/cloud/storage/service/gcp/GcpStorageServiceProvider.java b/cloud-storage-sdk-gcp/src/main/java/org/sunbird/cloud/storage/service/gcp/GcpStorageServiceProvider.java new file mode 100644 index 0000000..76d5c4d --- /dev/null +++ b/cloud-storage-sdk-gcp/src/main/java/org/sunbird/cloud/storage/service/gcp/GcpStorageServiceProvider.java @@ -0,0 +1,21 @@ +package org.sunbird.cloud.storage.service.gcp; + +import org.sunbird.cloud.storage.IStorageService; +import org.sunbird.cloud.storage.StorageConfig; +import org.sunbird.cloud.storage.StorageServiceProvider; + +/** + * SPI provider for Google Cloud Storage. + */ +public class GcpStorageServiceProvider implements StorageServiceProvider { + + @Override + public boolean supports(StorageConfig.StorageType type) { + return type == StorageConfig.StorageType.GCLOUD; + } + + @Override + public IStorageService create(StorageConfig config) { + return new GcpStorageService(config); + } +} diff --git a/cloud-storage-sdk-gcp/src/main/resources/META-INF/services/org.sunbird.cloud.storage.StorageServiceProvider b/cloud-storage-sdk-gcp/src/main/resources/META-INF/services/org.sunbird.cloud.storage.StorageServiceProvider new file mode 100644 index 0000000..aa35ab4 --- /dev/null +++ b/cloud-storage-sdk-gcp/src/main/resources/META-INF/services/org.sunbird.cloud.storage.StorageServiceProvider @@ -0,0 +1 @@ +org.sunbird.cloud.storage.service.gcp.GcpStorageServiceProvider diff --git a/cloud-storage-sdk-oci/pom.xml b/cloud-storage-sdk-oci/pom.xml new file mode 100644 index 0000000..f2c4777 --- /dev/null +++ b/cloud-storage-sdk-oci/pom.xml @@ -0,0 +1,41 @@ + + 4.0.0 + + org.sunbird + cloud-storage-sdk-parent + 2.0.0 + + + cloud-storage-sdk-oci + jar + Cloud Storage SDK - OCI + Oracle Cloud Infrastructure Object Storage implementation using OCI Java SDK. + + + + org.sunbird + cloud-storage-sdk-api + + + com.oracle.oci.sdk + oci-java-sdk-objectstorage + + + com.oracle.oci.sdk + oci-java-sdk-common-httpclient-jersey3 + runtime + + + org.junit.jupiter + junit-jupiter + test + + + org.mockito + mockito-core + test + + + diff --git a/cloud-storage-sdk-oci/src/main/java/org/sunbird/cloud/storage/service/oci/OciStorageService.java b/cloud-storage-sdk-oci/src/main/java/org/sunbird/cloud/storage/service/oci/OciStorageService.java new file mode 100644 index 0000000..bb84582 --- /dev/null +++ b/cloud-storage-sdk-oci/src/main/java/org/sunbird/cloud/storage/service/oci/OciStorageService.java @@ -0,0 +1,385 @@ +package org.sunbird.cloud.storage.service.oci; + +import com.oracle.bmc.Region; +import com.oracle.bmc.auth.AuthenticationDetailsProvider; +import com.oracle.bmc.auth.InstancePrincipalsAuthenticationDetailsProvider; +import com.oracle.bmc.auth.SimpleAuthenticationDetailsProvider; +import com.oracle.bmc.objectstorage.ObjectStorageClient; +import com.oracle.bmc.objectstorage.model.CreateBucketDetails; +import com.oracle.bmc.objectstorage.model.CreatePreauthenticatedRequestDetails; +import com.oracle.bmc.objectstorage.model.ListObjects; +import com.oracle.bmc.objectstorage.model.ObjectSummary; +import com.oracle.bmc.objectstorage.requests.*; +import com.oracle.bmc.objectstorage.responses.*; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.sunbird.cloud.storage.AbstractStorageService; +import org.sunbird.cloud.storage.StorageConfig; +import org.sunbird.cloud.storage.exception.StorageServiceException; + +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.InputStream; +import java.security.MessageDigest; +import java.util.ArrayList; +import java.util.Base64; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Oracle Cloud Infrastructure Object Storage implementation using the OCI Java SDK. + */ +public class OciStorageService extends AbstractStorageService { + + private static final Logger logger = LoggerFactory.getLogger(OciStorageService.class); + + private final StorageConfig config; + private final ObjectStorageClient objectStorageClient; + private final String namespace; + + public OciStorageService(StorageConfig config) { + this.config = config; + + AuthenticationDetailsProvider authProvider = resolveAuth(config); + + ObjectStorageClient.Builder clientBuilder = ObjectStorageClient.builder(); + + if (config.getEndPoint() != null && !config.getEndPoint().isEmpty()) { + clientBuilder.endpoint(config.getEndPoint()); + } else if (config.getRegion() != null && !config.getRegion().isEmpty()) { + clientBuilder.region(Region.fromRegionId(config.getRegion())); + } + + this.objectStorageClient = clientBuilder.build(authProvider); + + // Retrieve namespace + GetNamespaceResponse namespaceResponse = objectStorageClient.getNamespace( + GetNamespaceRequest.builder().build()); + this.namespace = namespaceResponse.getValue(); + + logger.info("Initialized OciStorageService, namespace={}, authType={}", + namespace, config.getAuthType()); + } + + private AuthenticationDetailsProvider resolveAuth(StorageConfig config) { + switch (config.getAuthType()) { + case ACCESS_KEY: + // For API Key auth, storageKey is the user OCID, storageSecret is the private key PEM + // This requires additional config values typically set via environment or system properties + String tenancyId = System.getenv().getOrDefault("OCI_TENANCY_ID", + System.getProperty("oci.tenancy.id", "")); + String userId = config.getStorageKey(); + String fingerprint = System.getenv().getOrDefault("OCI_FINGERPRINT", + System.getProperty("oci.fingerprint", "")); + String privateKey = config.getStorageSecret(); + String region = config.getRegion() != null ? config.getRegion() : "us-ashburn-1"; + + return SimpleAuthenticationDetailsProvider.builder() + .tenantId(tenancyId) + .userId(userId) + .fingerprint(fingerprint) + .privateKeySupplier(() -> new ByteArrayInputStream(privateKey.getBytes())) + .region(Region.fromRegionId(region)) + .build(); + + case OIDC: + case IAM: + case IAM_ROLE: + case INSTANCE_PROFILE: + return InstancePrincipalsAuthenticationDetailsProvider.builder().build(); + + default: + throw new StorageServiceException("Unsupported auth type: " + config.getAuthType()); + } + } + + @Override + protected void ensureContainerExists(String container) { + try { + objectStorageClient.headBucket(HeadBucketRequest.builder() + .namespaceName(namespace) + .bucketName(container) + .build()); + } catch (com.oracle.bmc.model.BmcException e) { + if (e.getStatusCode() == 404) { + try { + objectStorageClient.createBucket(CreateBucketRequest.builder() + .namespaceName(namespace) + .createBucketDetails(CreateBucketDetails.builder() + .name(container) + .compartmentId(System.getenv().getOrDefault( + "OCI_COMPARTMENT_ID", + System.getProperty("oci.compartment.id", ""))) + .build()) + .build()); + logger.info("Created bucket: {}", container); + } catch (Exception ex) { + logger.debug("Bucket creation for {}: {}", container, ex.getMessage()); + } + } + } + } + + @Override + protected String putObject(String container, String objectKey, File file) { + try (FileInputStream fis = new FileInputStream(file)) { + String contentType = tika.detect(file); + String md5 = computeMd5Base64(file); + + PutObjectRequest request = PutObjectRequest.builder() + .namespaceName(namespace) + .bucketName(container) + .objectName(objectKey) + .contentType(contentType) + .contentLength(file.length()) + .contentMD5(md5) + .putObjectBody(fis) + .build(); + objectStorageClient.putObject(request); + return buildObjectUri(container, objectKey); + } catch (Exception e) { + throw new StorageServiceException( + "Failed to put object from file: " + objectKey + " - " + e.getMessage(), e); + } + } + + @Override + protected String putObject(String container, String objectKey, byte[] content) { + try { + String md5 = computeMd5Base64(content); + PutObjectRequest request = PutObjectRequest.builder() + .namespaceName(namespace) + .bucketName(container) + .objectName(objectKey) + .contentLength((long) content.length) + .contentMD5(md5) + .putObjectBody(new ByteArrayInputStream(content)) + .build(); + objectStorageClient.putObject(request); + return buildObjectUri(container, objectKey); + } catch (Exception e) { + throw new StorageServiceException( + "Failed to put object from bytes: " + objectKey + " - " + e.getMessage(), e); + } + } + + @Override + protected InputStream getObjectStream(String container, String objectKey) { + try { + GetObjectResponse response = objectStorageClient.getObject( + GetObjectRequest.builder() + .namespaceName(namespace) + .bucketName(container) + .objectName(objectKey) + .build()); + return response.getInputStream(); + } catch (Exception e) { + throw new StorageServiceException( + "Failed to get object stream: " + objectKey + " - " + e.getMessage(), e); + } + } + + @Override + protected String getObjectUri(String container, String objectKey) { + return buildObjectUri(container, objectKey); + } + + @Override + protected BlobDetail getObjectDetail(String container, String objectKey) { + try { + HeadObjectResponse response = objectStorageClient.headObject( + HeadObjectRequest.builder() + .namespaceName(namespace) + .bucketName(container) + .objectName(objectKey) + .build()); + + Map metadata = new HashMap<>(); + if (response.getOpcMeta() != null) { + metadata.putAll(response.getOpcMeta()); + } + metadata.put("uri", buildObjectUri(container, objectKey)); + metadata.put("Content-Type", response.getContentType()); + metadata.put("ETag", response.getETag()); + + long contentLength = response.getContentLength() != null ? response.getContentLength() : 0L; + Date lastModified = response.getLastModified() != null + ? Date.from(response.getLastModified().toInstant()) + : null; + + return new BlobDetail(objectKey, contentLength, lastModified, metadata, + buildObjectUri(container, objectKey)); + } catch (Exception e) { + throw new StorageServiceException( + "Failed to get object detail: " + objectKey + " - " + e.getMessage(), e); + } + } + + @Override + protected List listKeys(String container, String prefix) { + try { + List keys = new ArrayList<>(); + String nextStart = null; + + do { + ListObjectsRequest.Builder requestBuilder = ListObjectsRequest.builder() + .namespaceName(namespace) + .bucketName(container) + .prefix(prefix); + if (nextStart != null) { + requestBuilder.start(nextStart); + } + + ListObjectsResponse response = objectStorageClient.listObjects(requestBuilder.build()); + ListObjects listObjects = response.getListObjects(); + for (ObjectSummary summary : listObjects.getObjects()) { + keys.add(summary.getName()); + } + nextStart = listObjects.getNextStartWith(); + } while (nextStart != null); + + return keys; + } catch (Exception e) { + throw new StorageServiceException( + "Failed to list keys with prefix: " + prefix + " - " + e.getMessage(), e); + } + } + + @Override + protected void removeObjects(String container, List objectKeys) { + try { + for (String key : objectKeys) { + objectStorageClient.deleteObject(DeleteObjectRequest.builder() + .namespaceName(namespace) + .bucketName(container) + .objectName(key) + .build()); + } + } catch (Exception e) { + throw new StorageServiceException( + "Failed to remove objects: " + e.getMessage(), e); + } + } + + @Override + protected void copyObject(String fromContainer, String fromKey, + String toContainer, String toKey) { + try { + objectStorageClient.copyObject(CopyObjectRequest.builder() + .namespaceName(namespace) + .bucketName(fromContainer) + .copyObjectDetails(com.oracle.bmc.objectstorage.model.CopyObjectDetails.builder() + .sourceObjectName(fromKey) + .destinationNamespace(namespace) + .destinationBucket(toContainer) + .destinationObjectName(toKey) + .destinationRegion(config.getRegion() != null ? config.getRegion() : "us-ashburn-1") + .build()) + .build()); + } catch (Exception e) { + throw new StorageServiceException( + "Failed to copy object from " + fromContainer + "/" + fromKey + + " to " + toContainer + "/" + toKey + " - " + e.getMessage(), e); + } + } + + @Override + protected String generateSignedGetUrl(String container, String objectKey, int ttlSeconds) { + return generatePreauthenticatedRequest(container, objectKey, ttlSeconds, + CreatePreauthenticatedRequestDetails.AccessType.ObjectRead); + } + + @Override + protected String generateSignedPutUrl(String container, String objectKey, + int ttlSeconds, String contentType, + Map additionalParams) { + return generatePreauthenticatedRequest(container, objectKey, ttlSeconds, + CreatePreauthenticatedRequestDetails.AccessType.ObjectWrite); + } + + private String generatePreauthenticatedRequest(String container, String objectKey, + int ttlSeconds, + CreatePreauthenticatedRequestDetails.AccessType accessType) { + try { + Date expiry = new Date(System.currentTimeMillis() + (long) ttlSeconds * 1000); + + CreatePreauthenticatedRequestDetails details = CreatePreauthenticatedRequestDetails.builder() + .name("par-" + objectKey + "-" + System.currentTimeMillis()) + .objectName(objectKey) + .accessType(accessType) + .timeExpires(expiry) + .build(); + + CreatePreauthenticatedRequestResponse response = + objectStorageClient.createPreauthenticatedRequest( + CreatePreauthenticatedRequestRequest.builder() + .namespaceName(namespace) + .bucketName(container) + .createPreauthenticatedRequestDetails(details) + .build()); + + String accessUri = response.getPreauthenticatedRequest().getAccessUri(); + String endpoint = config.getEndPoint() != null ? config.getEndPoint() + : "https://objectstorage." + (config.getRegion() != null ? config.getRegion() : "us-ashburn-1") + + ".oraclecloud.com"; + return endpoint + accessUri; + } catch (Exception e) { + throw new StorageServiceException( + "Failed to generate pre-authenticated request for: " + objectKey + " - " + e.getMessage(), e); + } + } + + @Override + protected String getHdfsPrefix(String container) { + return "oci://" + container + "@" + namespace + "/"; + } + + @Override + public void close() { + try { + objectStorageClient.close(); + } catch (Exception e) { + logger.warn("Error closing OCI ObjectStorageClient", e); + } + } + + private String buildObjectUri(String container, String objectKey) { + String endpoint = config.getEndPoint(); + if (endpoint != null && !endpoint.isEmpty()) { + String base = endpoint.endsWith("/") ? endpoint.substring(0, endpoint.length() - 1) : endpoint; + return base + "/n/" + namespace + "/b/" + container + "/o/" + objectKey; + } + String region = config.getRegion() != null ? config.getRegion() : "us-ashburn-1"; + return "https://objectstorage." + region + ".oraclecloud.com/n/" + + namespace + "/b/" + container + "/o/" + objectKey; + } + + private String computeMd5Base64(File file) { + try { + MessageDigest md = MessageDigest.getInstance("MD5"); + try (FileInputStream fis = new FileInputStream(file)) { + byte[] buffer = new byte[8192]; + int read; + while ((read = fis.read(buffer)) != -1) { + md.update(buffer, 0, read); + } + } + return Base64.getEncoder().encodeToString(md.digest()); + } catch (Exception e) { + throw new StorageServiceException("Failed to compute MD5: " + e.getMessage(), e); + } + } + + private String computeMd5Base64(byte[] content) { + try { + MessageDigest md = MessageDigest.getInstance("MD5"); + md.update(content); + return Base64.getEncoder().encodeToString(md.digest()); + } catch (Exception e) { + throw new StorageServiceException("Failed to compute MD5: " + e.getMessage(), e); + } + } +} diff --git a/cloud-storage-sdk-oci/src/main/java/org/sunbird/cloud/storage/service/oci/OciStorageServiceProvider.java b/cloud-storage-sdk-oci/src/main/java/org/sunbird/cloud/storage/service/oci/OciStorageServiceProvider.java new file mode 100644 index 0000000..b28801e --- /dev/null +++ b/cloud-storage-sdk-oci/src/main/java/org/sunbird/cloud/storage/service/oci/OciStorageServiceProvider.java @@ -0,0 +1,21 @@ +package org.sunbird.cloud.storage.service.oci; + +import org.sunbird.cloud.storage.IStorageService; +import org.sunbird.cloud.storage.StorageConfig; +import org.sunbird.cloud.storage.StorageServiceProvider; + +/** + * SPI provider for Oracle Cloud Infrastructure Object Storage. + */ +public class OciStorageServiceProvider implements StorageServiceProvider { + + @Override + public boolean supports(StorageConfig.StorageType type) { + return type == StorageConfig.StorageType.OCI; + } + + @Override + public IStorageService create(StorageConfig config) { + return new OciStorageService(config); + } +} diff --git a/cloud-storage-sdk-oci/src/main/resources/META-INF/services/org.sunbird.cloud.storage.StorageServiceProvider b/cloud-storage-sdk-oci/src/main/resources/META-INF/services/org.sunbird.cloud.storage.StorageServiceProvider new file mode 100644 index 0000000..dc9c44b --- /dev/null +++ b/cloud-storage-sdk-oci/src/main/resources/META-INF/services/org.sunbird.cloud.storage.StorageServiceProvider @@ -0,0 +1 @@ +org.sunbird.cloud.storage.service.oci.OciStorageServiceProvider diff --git a/pom.xml b/pom.xml index ad226a2..7e4d119 100644 --- a/pom.xml +++ b/pom.xml @@ -1,33 +1,61 @@ - + 4.0.0 org.sunbird - cloud-store-sdk_${scala.maj.version} - 1.4.10 - jar - Cloud Store SDK - cloud-store-sdk provides client APIs to handle cloud store (AWS S3, Azure Blobstore, GCP, OCI). + cloud-storage-sdk-parent + 2.0.0 + pom + Cloud Storage SDK Parent + Multi-module cloud storage SDK providing CSP-specific implementations using official cloud SDKs. https://www.sunbird.org/ + - 1.8 - 1.8 + 11 + 11 + 11 UTF-8 - 1.4.4 - 2.16.0 + + + 2.25.0 + 12.25.0 + 1.11.0 + 2.30.0 + 3.30.0 + 2.9.0 + 2.15.3 + 2.0.9 + 5.10.1 + 5.8.0 + + + cloud-storage-sdk-api + cloud-storage-sdk-aws + cloud-storage-sdk-azure + cloud-storage-sdk-gcp + cloud-storage-sdk-oci + + - - MIT License - http://www.opensource.org/licenses/mit-license.php - - - - - Sunbird - info@sunbird.org - Sunbird - https://www.sunbird.org/ - - + + MIT License + http://www.opensource.org/licenses/mit-license.php + + + + + Sunbird + info@sunbird.org + Sunbird + https://www.sunbird.org/ + + + + scm:git:git://github.com/project-sunbird/sunbird-cloud-storage-sdk.git + scm:git:ssh://github.com/project-sunbird/sunbird-cloud-storage-sdk.git + https://github.com/project-sunbird/sunbird-cloud-storage-sdk/tree/master + central @@ -38,333 +66,189 @@ https://s01.oss.sonatype.org/service/local/staging/deploy/maven2/ - - scm:git:git://github.com/project-sunbird/sunbird-cloud-storage-sdk.git - scm:git:ssh://github.com/project-sunbird/sunbird-cloud-storage-sdk.git - https://github.com/project-sunbird/sunbird-cloud-storage-sdk/tree/master - - - - org.scala-lang - scala-library - ${scala.version} - provided - - - com.google.guava - guava - 31.1-jre - - - junit - junit - 3.8.1 - test - - - org.apache.jclouds - jclouds-all - 2.5.0 - - - com.jamesmurty.utils - java-xmlbuilder - - - org.yaml - snakeyaml - - - com.google.gson - gson - - - - - com.google.cloud - google-cloud-storage - ${gcp.storage.version} - - - org.yaml - snakeyaml - 2.0 - - - com.jamesmurty.utils - java-xmlbuilder - 1.2 - - - com.google.code.gson - gson - 2.8.9 - - - com.microsoft.azure - azure-storage - 5.0.0 - - - jackson-core - com.fasterxml.jackson.core - - - - - org.apache.hadoop - hadoop-azure - 2.7.3 - - - org.apache.hadoop - hadoop-common - - - jackson-core - com.fasterxml.jackson.core - - - - - org.apache.hadoop - hadoop-common - 3.3.6 - - - jackson-databind - com.fasterxml.jackson.core - - - com.google.protobuf - protobuf-java - - - org.codehaus.jettison - jettison - - - org.xerial.snappy - snappy-java - - - org.apache.avro - avro - - - - - org.apache.avro - avro - 1.11.3 - - - org.xerial.snappy - snappy-java - 1.1.10.4 - - - org.codehaus.jettison - jettison - 1.5.4 - - - com.google.protobuf - protobuf-java - 3.21.7 - - - com.typesafe - config - 1.3.0 - - - joda-time - joda-time - 2.8.1 - - - org.scalatest - scalatest_${scala.maj.version} - 3.2.15 - test - - - com.fasterxml.jackson.core - jackson-databind - 2.13.5 - - - com.fasterxml.jackson.module - jackson-module-scala_${scala.maj.version} - 2.13.5 - - - org.apache.tika - tika-core - 2.9.0 - - - - software.amazon.awssdk - auth - 2.25.0 - - - - software.amazon.awssdk - sts - 2.25.0 - - - - org.scala-lang.modules - scala-collection-compat_${scala.maj.version} - 2.11.0 - - + + + + central + https://repo1.maven.org/maven2 + + + + + central + https://repo1.maven.org/maven2 + + + + + + + + org.sunbird + cloud-storage-sdk-api + ${project.version} + + + + + software.amazon.awssdk + s3 + ${aws.sdk.version} + + + software.amazon.awssdk + sts + ${aws.sdk.version} + + + + + org.apache.tika + tika-core + ${tika.version} + + + org.slf4j + slf4j-api + ${slf4j.version} + + + com.fasterxml.jackson.core + jackson-databind + ${jackson.version} + + + + + com.azure + azure-storage-blob + ${azure.storage.blob.version} + + + com.azure + azure-identity + ${azure.identity.version} + + + + + com.google.cloud + google-cloud-storage + ${gcp.storage.version} + + + + + com.oracle.oci.sdk + oci-java-sdk-objectstorage + ${oci.sdk.version} + + + com.oracle.oci.sdk + oci-java-sdk-common-httpclient-jersey3 + ${oci.sdk.version} + + + + + org.junit.jupiter + junit-jupiter + ${junit5.version} + test + + + org.mockito + mockito-core + ${mockito.version} + test + + + + - src/main/scala - src/test/scala - - - - net.alchim31.maven - scala-maven-plugin - 4.4.0 - - - - compile - testCompile - - - - -dependencyfile - ${project.build.directory}/.scala_dependencies - - - - - attach-javadocs - - doc-jar - - - - - - org.scalatest - scalatest-maven-plugin - 1.0 - - - test - test - - test - - - - - - org.scoverage - scoverage-maven-plugin - ${scoverage.plugin.version} - - ${scala.version} - true - true - - - - org.apache.maven.plugins - maven-source-plugin - 2.2.1 - - - attach-sources - - jar-no-fork - - - - - - org.apache.maven.plugins - maven-gpg-plugin - 1.5 - - - sign-artifacts - deploy - - sign - - - - - --pinentry-mode - loopback - - - - - - - org.sonatype.central - central-publishing-maven-plugin - 0.7.0 - true - - central - - - - org.codehaus.mojo - flatten-maven-plugin - 1.5.0 - - ossrh - - - - flatten - process-resources - - flatten - - - - flatten.clean - clean - - clean - - - - - - - - - - scala-2.13 - - true - - - 2.13 - 2.13.12 - - - - - scala-2.12 - - 2.12 - 2.12.18 - - - + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.11.0 + + ${java.version} + ${java.version} + UTF-8 + + + + org.apache.maven.plugins + maven-surefire-plugin + 3.2.3 + + + org.apache.maven.plugins + maven-source-plugin + 3.3.0 + + + attach-sources + + jar-no-fork + + + + + + org.apache.maven.plugins + maven-javadoc-plugin + 3.6.3 + + + attach-javadocs + + jar + + + + + + org.apache.maven.plugins + maven-gpg-plugin + 1.5 + + + sign-artifacts + deploy + + sign + + + + --pinentry-mode + loopback + + + + + + + org.sonatype.central + central-publishing-maven-plugin + 0.7.0 + true + + central + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + + org.apache.maven.plugins + maven-surefire-plugin + + + diff --git a/src/main/scala/org/sunbird/cloud/storage/BaseStorageService.scala b/src/main/scala/org/sunbird/cloud/storage/BaseStorageService.scala deleted file mode 100644 index 12af232..0000000 --- a/src/main/scala/org/sunbird/cloud/storage/BaseStorageService.scala +++ /dev/null @@ -1,331 +0,0 @@ -package org.sunbird.cloud.storage - -import org.jclouds.blobstore._ -import com.google.common.io._ -import java.io._ - -import org.apache.commons.io.FilenameUtils -import org.apache.tika.Tika -import org.sunbird.cloud.storage.exception.StorageServiceException -import org.sunbird.cloud.storage.util.{CommonUtil, JSONUtils} - -import scala.jdk.CollectionConverters._ -import org.jclouds.blobstore.options.ListContainerOptions.Builder.{afterMarker, prefix, recursive} -import org.sunbird.cloud.storage.Model.Blob -import org.jclouds.blobstore.options.{CopyOptions, PutOptions} -import org.sunbird.cloud.storage.conf.AppConf - -import scala.collection.mutable.ListBuffer -import scala.concurrent.{ExecutionContext, Future} - -trait BaseStorageService extends IStorageService { - - var context: BlobStoreContext - var blobStore: BlobStore - var maxRetries: Int = 2 - var maxSignedurlTTL: Int = 604800 - var attempt = 0 - var maxContentLength = 0 - val tika = new Tika() - - def filesList(file: File): List[File] = { - if (file.exists && file.isDirectory) { - val files = file.listFiles.filter(_.isDirectory).flatMap(f => filesList(f)).toList - files ++ file.listFiles.filter(_.isFile).toList; - } else if (file.exists && file.isFile) { - List[File](file) - } else { - List[File](); - } - } - - override def uploadFolder(container: String, file: String, objectKey: String, isPublic: Option[Boolean] = Option(false), ttl: Option[Int] = None, retryCount: Option[Int] = None, attempt: Int = 1) (implicit execution: ExecutionContext) : Future[List[String]] = { - val d = new File(file) - val files = filesList(d) - val futures = files.map {f => - Future { - val key = objectKey + f.getAbsolutePath.split(d.getAbsolutePath + File.separator).last - upload(container, f.getAbsolutePath, key, Option(false), Option(attempt), retryCount, ttl) - } - }; - Future.sequence(futures); - } - - def createContainerInLocation(container: String): Unit = { - blobStore.createContainerInLocation(null, container) - } - - def putBlob(objectKey: String, file: File, container: String): Unit = { - val payload = Files.asByteSource(file) - val contentType = tika.detect(file) - val blob = blobStore.blobBuilder(objectKey).payload(payload).contentType(contentType).contentEncoding("UTF-8").contentLength(payload.size()).build() - blobStore.putBlob(container, blob, new PutOptions().multipart()) - } - - override def upload(container: String, file: String, objectKey: String, isDirectory: Option[Boolean] = Option(false), attempt: Option[Int] = Option(1), retryCount: Option[Int] = None, ttl: Option[Int] = None): String = { - try { - if(isDirectory.get) { - val d = new File(file) - val files = filesList(d) - val list = files.map {f => - val key = objectKey + f.getAbsolutePath.split(d.getAbsolutePath + File.separator).last - upload(container, f.getAbsolutePath, key, Option(false), attempt, retryCount, ttl) - } - list.mkString(",") - } - else { - if (attempt.getOrElse(1) >= retryCount.getOrElse(maxRetries)) { - val message = s"Failed to upload. file: $file, key: $objectKey, attempt: $attempt, maxAttempts: $retryCount. Exceeded maximum number of retries" - throw new StorageServiceException(message) - } - - createContainerInLocation(container) - val fileObj = new File(file) - putBlob(objectKey, fileObj, container) - if (ttl.isDefined) { - getSignedURL(container, objectKey, Option(ttl.get)) - } else - blobStore.blobMetadata(container, objectKey).getUri.toString - } - } - catch { - case e: Exception => { - e.printStackTrace() - Thread.sleep(attempt.getOrElse(1)*2000) - val uploadAttempt = attempt.getOrElse(1) + 1 - if (uploadAttempt <= retryCount.getOrElse(maxRetries)) { - upload(container, file, objectKey, isDirectory, Option(uploadAttempt), retryCount, ttl) - } else { - throw e; - } - } - } - } - - override def put(container: String, content: Array[Byte], objectKey: String, isPublic: Option[Boolean] = Option(false), isDirectory: Option[Boolean] = Option(false), ttl: Option[Int] = None, retryCount: Option[Int] = None): String = { - - try { - - if (attempt == retryCount.getOrElse(maxRetries)) { - val message = s"Failed to upload. key: $objectKey, attempt: $attempt, maxAttempts: $retryCount. Exceeded maximum number of retries" - throw new StorageServiceException(message) - } - - createContainerInLocation(container) - val blob = blobStore.blobBuilder(objectKey).payload(content).contentLength(content.length).build() - blobStore.putBlob(container, blob, new PutOptions().multipart()) - if(isPublic.get) { - getSignedURL(container, objectKey, Option(ttl.getOrElse(maxSignedurlTTL))) - } - else blobStore.getBlob(container, objectKey).getMetadata.getUri.toString - } - catch { - case e: Exception => { - Thread.sleep(attempt*2000) - attempt += 1 - put(container, content, objectKey, isPublic, isDirectory, ttl, retryCount) - } - } - } - - override def getSignedURL(container: String, objectKey: String, ttl: Option[Int] = None, permission: Option[String] = Option("r")): String = { - if (context.getBlobStore.toString.contains("google")) { - throw new StorageServiceException("getSignedURL method is not supported for GCP. Please use getPutSignedURL with contentType.", new Exception()) - } - else { - if (permission.getOrElse("").equalsIgnoreCase("w")) { - context.getSigner.signPutBlob(container, blobStore.blobBuilder(objectKey).forSigning().contentLength(maxContentLength).build(), ttl.getOrElse(maxSignedurlTTL).asInstanceOf[Number].longValue()).getEndpoint.toString - } else { - context.getSigner.signGetBlob(container, objectKey, ttl.getOrElse(maxSignedurlTTL)).getEndpoint.toString - } - } - } - - override def getSignedURLV2(container: String, objectKey: String, ttl: Option[Int] = None, permission: Option[String] = Option("r"), - contentType: Option[String] = Option("application/octet-stream"), additionalParams: Option[Map[String,String]] = None): String = { - if (context.getBlobStore.toString.contains("google")) { - getPutSignedURL(container, objectKey, Option(maxSignedurlTTL), None, contentType, additionalParams) - } else { - getSignedURL(container, objectKey, ttl, permission) - } - } - def getPutSignedURL(container: String, objectKey: String, ttl: Option[Int] = None, permission: Option[String] = Option("r"), - contentType: Option[String] = Option("application/octet-stream"), additionalParams: Option[Map[String,String]] = None): String = { - if (permission.getOrElse("").equalsIgnoreCase("w")) { - context.getSigner.signPutBlob(container, blobStore.blobBuilder(objectKey).forSigning().contentLength(maxContentLength).contentType(contentType.get).build(), ttl.getOrElse(maxSignedurlTTL).asInstanceOf[Number].longValue()).getEndpoint.toString - } else { - context.getSigner.signGetBlob(container, objectKey, ttl.getOrElse(maxSignedurlTTL)).getEndpoint.toString - } - } - - override def download(container: String, objectKey: String, localPath: String, isDirectory: Option[Boolean] = Option(false)) = { - try { - if(isDirectory.get) { - val objects = listObjectKeys(container, objectKey) - for (obj <- objects) { - val file = FilenameUtils.getName(obj); - val fileObj = blobStore.getBlob(container, obj) - val downloadPath = localPath + FilenameUtils.getPath(obj).split("/").last + "/"; - CommonUtil.copyFile(fileObj.getPayload.getInput, downloadPath.replaceAll("//", "/"), file); - } - } - else { - val inStream = blobStore.getBlob(container, objectKey).getPayload.getInput - val fileName = objectKey.split("/").last - CommonUtil.copyFile(inStream, localPath, fileName); - } - } catch { - case e: Exception => - throw new StorageServiceException(e.getMessage, e) - } - } - - override def deleteObject(container: String, objectKey: String, isDirectory: Option[Boolean] = Option(false)) = { - try { - deleteObjects(container, List((objectKey, isDirectory.get))) - } catch { - case e: Exception => - throw new StorageServiceException(e.getMessage, e) - } - } - - override def deleteObjects(container: String, objectKeys: List[(String, Boolean)]) = { - try { - for (obj <- objectKeys) { - if(obj._2) { - val objList = blobStore.list(container, prefix(obj._1).recursive()).asScala.map(f => f.getName).toList - blobStore.removeBlobs(container, objList.asJavaCollection) - } - else { - blobStore.removeBlobs(container, List(obj._1).asJavaCollection) - } - } - } catch { - case e: Exception => - throw new StorageServiceException(e.getMessage, e) - } - } - - override def getObject(container: String, objectKey: String, withPayload: Option[Boolean] = Option(false)): Blob = { - try { - val blob = blobStore.getBlob(container, objectKey) - val objData = blob.getMetadata - val metaData = JSONUtils.deserialize[Map[String, AnyRef]](JSONUtils.serialize(objData)) - val payload = if(withPayload.get) Option(blob.getPayload.getContentMetadata.getContentMD5AsHashCode.asBytes()) else None - Blob(objectKey, objData.getContentMetadata.getContentLength, objData.getLastModified, metaData, payload) - } catch { - case e: Exception => - throw new StorageServiceException(e.getMessage, e) - } - } - - override def listObjects(container: String, prefix: String, withPayload: Option[Boolean] = Option(false)): List[Blob] = { - try { - val objects = listObjectKeys(container, prefix) - objects.map { obj => - getObject(container, obj, withPayload) - } - } - catch { - case e: Exception => - throw new StorageServiceException(e.getMessage, e) - } - } - - override def listObjectKeys(container: String, _prefix: String): List[String] = { - val fileNames = ListBuffer[String]() - val containerOpts = recursive().prefix(_prefix) - var marker: Option[String] = None - do { - if (marker.exists(_.trim.nonEmpty)) containerOpts.afterMarker(marker.getOrElse("")) - val pageSet = blobStore.list(container, containerOpts) - fileNames ++= pageSet.asScala.map(f => f.getName).toList - marker = Option(pageSet.getNextMarker) - } while (marker.isDefined) - fileNames.toList - } - - override def searchObjects(container: String, prefix: String, fromDate: Option[String] = None, toDate: Option[String] = None, delta: Option[Int] = None, pattern: String = "yyyy-MM-dd"): List[Blob] = { - val from = if (delta.nonEmpty) CommonUtil.getStartDate(toDate, delta.get) else fromDate; - if (from.nonEmpty) { - val dates = CommonUtil.getDatesBetween(from.get, toDate, pattern) - val paths = for (date <- dates) yield { - listObjects(container, prefix + date) - } - paths.flatMap { x => x.map { x => x } }.toList - } else { - listObjects(container, prefix) - } - } - - override def searchObjectkeys(container: String, prefix: String, fromDate: Option[String] = None, toDate: Option[String] = None, delta: Option[Int] = None, pattern: String = "yyyy-MM-dd"): List[String] = { -// val objectList = searchObjects(container, prefix, fromDate, toDate, delta, pattern) -// getPaths(container, objectList); - val from = if (delta.nonEmpty) CommonUtil.getStartDate(toDate, delta.get) else fromDate; - if (from.nonEmpty) { - val dates = CommonUtil.getDatesBetween(from.get, toDate, pattern); - val paths = for (date <- dates) yield { - listObjectKeys(container, prefix + date) - } - paths.flatMap { x => x.map { x => x } }.toList - } else { - listObjectKeys(container, prefix) - } - } - - override def copyObjects(fromContainer: String, fromKey: String, toContainer: String, toKey: String, isDirectory: Option[Boolean] = Option(false)): Unit = { - if(isDirectory.get) { - val updatedFromKey = if(fromKey.endsWith("/")) fromKey else fromKey+"/" - val updatedToKey = if(toKey.endsWith("/")) toKey else toKey+"/" - val objectKeys = listObjectKeys(fromContainer, updatedFromKey) - for (obj <- objectKeys) { - val objName = obj.replace(updatedFromKey, "") - blobStore.copyBlob(fromContainer, obj, toContainer, updatedToKey+objName, CopyOptions.NONE) - } - } - else blobStore.copyBlob(fromContainer, fromKey, toContainer, toKey, CopyOptions.NONE) - } - - override def extractArchive(container: String, objectKey: String, toKey: String): Unit = { - try { - val localPath = AppConf.getConfig("local_extract_path") - download(container, objectKey, localPath, Option(false)) - val localFolder = localPath + "/" + toKey.split("/").last - CommonUtil.unZip(localPath + "/" + objectKey.split("/").last, localFolder) - upload(container, localFolder, toKey, Option(true)) - } - catch { - case e: Exception => - throw new StorageServiceException(e.getMessage, e) - } - } - - override def getObjectData(container: String, objectKey: String): Array[String] = { - - try { - val inStream = blobStore.getBlob(container, objectKey).getPayload.openStream() - scala.io.Source.fromInputStream(inStream).getLines().toArray - } catch { - case e: Exception => - throw new StorageServiceException(e.getMessage, e) - } - } - - override def getUri(container: String, _prefix: String, isDirectory: Option[Boolean] = Option(false)): String = { - val keys = listObjectKeys(container, _prefix); - if (keys.isEmpty) - throw new StorageServiceException("The given _prefix is incorrect: " + _prefix) - val prefix = keys.head - val blob = getObject(container, prefix, Option(false)) - val uri = blob.metadata.get("uri") - if (!uri.isEmpty) { - uri.get.asInstanceOf[String].split(_prefix).head + _prefix - } else - throw new StorageServiceException("uri not available for the given prefix: "+ _prefix) - } - - def closeContext() = { - context.close() - } - -} \ No newline at end of file diff --git a/src/main/scala/org/sunbird/cloud/storage/IStorageService.scala b/src/main/scala/org/sunbird/cloud/storage/IStorageService.scala deleted file mode 100644 index bc5312f..0000000 --- a/src/main/scala/org/sunbird/cloud/storage/IStorageService.scala +++ /dev/null @@ -1,213 +0,0 @@ -package org.sunbird.cloud.storage - -import org.sunbird.cloud.storage.Model.Blob - -import scala.concurrent.{ExecutionContext, Future} - -trait IStorageService { - - def uploadFolder(container: String, file: String, objectKey: String, isPublic: Option[Boolean] = Option(false), ttl: Option[Int] = None, retryCount: Option[Int] = None, attempt: Int = 1)(implicit execution: ExecutionContext) : Future[List[String]]; - - /** - * - * Upload a file/folder to cloud - * - * @param container String - The container/bucket to upload the file to. - * @param file String - The file path. - * @param objectKey String - The destination key/path to upload the file to. If the path exists it will be overwritten. - * @param isDirectory Option[Boolean] - Whether the file is a directory and need to upload folder recursively? - * @param attempt - * @param retryCount Option[Int] - Number of times the upload will be retried before failing. Defaults to global configuration "max.retries" - * @param ttl Option[Int] - The ttl/expiry for the file. Optional and default is never expires - * @return String - The url of the file/folder uploaded - */ - def upload(container: String, file: String, objectKey: String, isDirectory: Option[Boolean] = Option(false), attempt: Option[Int] = Option(1), retryCount: Option[Int] = None, ttl: Option[Int] = None): String - - - /** - * Put a blob in the cloud with the given content data. The difference between this and upload() method is that this method takes in byte array and sets is payload for the object. - * - * @param container String - The container/bucket to upload the file to. - * @param content String - The byte array of an object. - * @param objectKey String - The destination key/path to upload the file to. If the path exists it will be overwritten. - * @param isPublic Option[Boolean] - Whether the file should have public read access? Optional and defaults to false. - * @param isDirectory Option[Boolean] - Whether the file is a directory and need to upload folder recursively? Optional and defaults to false. - * @param ttl Option[Int] - The ttl/expiry for the file. Optional and default is never expires - * @param retryCount Option[Int] - Number of times the upload will be retried before failing. Defaults to "max.retries" defined in global configuration - * - * @return String - The url of the file/folder uploaded - */ - def put(container: String, content: Array[Byte], objectKey: String, isPublic: Option[Boolean] = Option(false), isDirectory: Option[Boolean] = Option(false), - ttl: Option[Int] = None, retryCount: Option[Int] = None): String - - /** - * Get pre-signed URL to access an object in the cloud store. - * - * @param container String - The container/bucket of the file - * @param objectKey String - The key/path of the file to pre-sign - * @param ttl Option[Int] - The ttl/expiry for the pre-signed URL. Defaults to "max.signedurl.ttl" defined in global configuration* - * @param permission String - The permission of pre-signed url the values are w (write), r (read). Defaults to "read". - * - * @return String - The pre-signed url - */ - def getSignedURL(container: String, objectKey: String, ttl: Option[Int] = None, permission: Option[String] = Option("r")): String - - /** - * Get pre-signed URL to access an object in the cloud store. - * - * @param container String - The container/bucket of the file - * @param objectKey String - The key/path of the file to pre-sign - * @param ttl Option[Int] - The ttl/expiry for the pre-signed URL. Defaults to "max.signedurl.ttl" defined in global configuration* - * @param permission String - The permission of pre-signed url the values are w (write), r (read). Defaults to "read". - * @param contentType String - The object type of the content. - * - * @return String - The pre-signed url - */ - def getSignedURLV2(container: String, objectKey: String, ttl: Option[Int] = None, permission: Option[String] = Option("r"), contentType: Option[String] = Option("text/plain"), additionalParams: Option[Map[String, String]] = None): String - - /** - * Download file/folder from cloud storage - * - * @param container String - The container/bucket of the file - * @param objectKey String - The key/path of the file to download from - * @param localPath String - The local destination path to download to - * @param isDirectory Option[Boolean] - Whether the file is a directory and need to be downloaded recursively? Optional and defaults to false. - */ - def download(container: String, objectKey: String, localPath: String, isDirectory: Option[Boolean] = Option(false)) - - /** - * Delete an object from the cloud store - * - * @param container String - The container/bucket of the object. - * @param objectKey String - The object key to delete. - * @param isDirectory Option[Boolean] - Whether the object is a directory and need to be deleted recursively? Optional and defaults to false. - */ - def deleteObject(container: String, objectKey: String, isDirectory: Option[Boolean] = Option(false)) - - /** - * Delete objects from the cloud store - * - * @param container String - The container/bucket of the objects - * @param objectKeys List[(String, Boolean)] - The objects to delete. The tuple contains the object key to be deleted and whether the object is a folder so that recursive delete is triggered. - */ - def deleteObjects(container: String, objectKeys: List[(String, Boolean)]) - - /** - * Copy objects from one container to another container or between different folders within the same container. This assumes that the credentials provided have access to both containers. - * - * @param fromContainer String - The container to copy the object from. - * @param fromKey String - The object prefix to copy from. - * @param toContainer String - The container to copy the object to. - * @param toKey String - The object prefix to copy to. - * @param isDirectory Option[Boolean] - Whether the copy is a file or folder? Defaults to false i.e copy one file. - */ - def copyObjects(fromContainer: String, fromKey: String, toContainer: String, toKey: String, isDirectory: Option[Boolean] = Option(false)) - - - /** - * Remote extract a archived file on cloud storage to a given folder within the same container. - * - * @param container String - The container/bucket of the archive file. - * @param objectKey String - The blob object archive file - * @param toKey String - The destination folder on the bucket/container to extract to. - */ - def extractArchive(container: String, objectKey: String, toKey: String) - - /** - * Get the blob object details - * - * @param container String - The container/bucket of the file - * @param objectKey String - The key/path of the blob object - * @param withPayload Option[Boolean] - Get payload as well while fetching details. Defaults to false. - * - * @return Blob - The blob object. - */ - def getObject(container: String, objectKey: String, withPayload: Option[Boolean] = Option(false)): Blob - - /** - * List objects from cloud storage for a given prefix. - * - * @param container String - The container/bucket - * @param prefix String - The object prefix to list objects. The prefix can be folder or pattern. - * @param withPayload Option[Boolean] - Does the listing of objects include payload as well? Defaults to false - * - * @return List[Blob] - The blob objects for the given prefix. - */ - def listObjects(container: String, prefix: String, withPayload: Option[Boolean] = Option(false)): List[Blob] - - /** - * List object keys from cloud storage for a given prefix. Similar to listObjects() - * - * @param container String - The container/bucket - * @param _prefix String - The object prefix to list objects. The prefix can be folder or pattern. - * - * @return List[Blob] - The blob objects for the given prefix. - */ - def listObjectKeys(container: String, _prefix: String): List[String] - - /** - * Search for objects for a given prefix and return only keys. Specifically used for telemetry files as the files are prefixed by sync date. - * By design the payload is not included in the search. If payload is required use getObject() method after the search is complete. - * - * // TODO: Provide examples of all combinations of fromDate, toDate and delta - * - * @param container String - The container/bucket. - * @param prefix String - The prefix to search on. - * @param fromDate Option[String] - The date to search from. Optional - * @param toDate Option[String] - The date to search to. Optional - * @param delta Option[Int] - The delta to search from given a from date or to date. Optional. If delta is provided and both fromDate and toDate are empty, the toDate will be defaulted to current date - * @param pattern String - The date pattern of from and to date. Defaulst to "yyyy-MM-dd" - * - * @return List[Blob] - The object keys - */ - def searchObjects(container: String, prefix: String, fromDate: Option[String] = None, toDate: Option[String] = None, delta: Option[Int] = None, - pattern: String = "yyyy-MM-dd"): List[Blob] - - /** - * Similar to searchObjects(). The only difference is that this method only returns the object keys instead of full blob objects. - * - * @param container String - The container/bucket. - * @param prefix String - The prefix to search on. - * @param fromDate Option[String] - The date to search from. Optional - * @param toDate Option[String] - The date to search to. Optional - * @param delta Option[Int] - The delta to search from given a from date or to date. Optional. If delta is provided and both fromDate and toDate are empty, the toDate will be defaulted to current date - * @param pattern String - The date pattern of from and to date. Defaulst to "yyyy-MM-dd" - * - * @return List[String] - The object keys - */ - def searchObjectkeys(container: String, prefix: String, fromDate: Option[String] = None, toDate: Option[String] = None, delta: Option[Int] = None, - pattern: String = "yyyy-MM-dd"): List[String] - - /** - * Get HDFS compatible file paths to be used in tech stack like Spark. - * For ex: for S3 the file path is prefixed with s3n:/// and for Azure blob storage it would be wasbs://@.blob.core.windows.net// - * - * @param container String - The container/bucket of the objects - * @param objects List[Blob] - The Blob objects in the given container - * - * @return List[String] - HDFS compatible file paths. - */ - def getPaths(container: String, objects: List[Blob]): List[String] - - /** - * Get the blob object data. - * - * @param container String - The container/bucket of the file - * @param objectKey String - The key/path of the blob object - * - * @return Array[String] - object data - */ - def getObjectData(container: String, objectKey: String): Array[String] - - - /** - * Get the URI of the given prefix - * - * @param container - * @param _prefix - * @param isDirectory - * @return String - URI of the given prefix - */ - def getUri(container: String, _prefix: String, isDirectory: Option[Boolean] = Option(false)): String - -} \ No newline at end of file diff --git a/src/main/scala/org/sunbird/cloud/storage/Model.scala b/src/main/scala/org/sunbird/cloud/storage/Model.scala deleted file mode 100644 index 5f8739d..0000000 --- a/src/main/scala/org/sunbird/cloud/storage/Model.scala +++ /dev/null @@ -1,8 +0,0 @@ -package org.sunbird.cloud.storage - -import java.util.Date - -object Model { - - case class Blob(key: String, contentLength: Long, lastModified: Date, metadata: Map[String, AnyRef], payload: Option[Array[Byte]]); -} \ No newline at end of file diff --git a/src/main/scala/org/sunbird/cloud/storage/conf/AppConf.scala b/src/main/scala/org/sunbird/cloud/storage/conf/AppConf.scala deleted file mode 100644 index ab09939..0000000 --- a/src/main/scala/org/sunbird/cloud/storage/conf/AppConf.scala +++ /dev/null @@ -1,43 +0,0 @@ -package org.sunbird.cloud.storage.conf - -import com.typesafe.config.ConfigFactory -import com.typesafe.config.Config - -object AppConf { - - lazy val defaultConf = ConfigFactory.load(); - lazy val envConf = ConfigFactory.systemEnvironment(); - lazy val conf = envConf.withFallback(defaultConf); - - def getConfig(key: String): String = { - if (conf.hasPath(key)) - conf.getString(key); - else ""; - } - - def getConfig: Config = conf - - - def getStorageType: String = getConfig("cloud_storage_type") - - def getStorageKey: String = getConfig("cloud_storage_key") - - def getStorageSecret: String = getConfig("cloud_storage_secret") - - def getAuthType: String = { - val authType = getConfig("cloud_storage_auth_type") - if (authType.nonEmpty) authType else "access_key" - } - - def getRegion: Option[String] = { - if (getStorageType.equals("oci")) - Option(getConfig("cloud_storage_region")) - else Option("") - } - - def getEndPoint: Option[String] = { - if (getStorageType.equals("oci")) - Option(getConfig("cloud_storage_endpoint")) - else Option("") - } -} diff --git a/src/main/scala/org/sunbird/cloud/storage/conf/package.scala b/src/main/scala/org/sunbird/cloud/storage/conf/package.scala deleted file mode 100644 index 6d0bd94..0000000 --- a/src/main/scala/org/sunbird/cloud/storage/conf/package.scala +++ /dev/null @@ -1,7 +0,0 @@ -package org.sunbird.cloud.storage -/** - * Provides the configuration handling APIs. - * @since 1.0 - */ -package object conf { -} \ No newline at end of file diff --git a/src/main/scala/org/sunbird/cloud/storage/exception/StorageServiceException.scala b/src/main/scala/org/sunbird/cloud/storage/exception/StorageServiceException.scala deleted file mode 100644 index d13cef7..0000000 --- a/src/main/scala/org/sunbird/cloud/storage/exception/StorageServiceException.scala +++ /dev/null @@ -1,5 +0,0 @@ -package org.sunbird.cloud.storage.exception - -class StorageServiceException(msg: String, ex: Exception = null) extends Exception(msg, ex) { - -} diff --git a/src/main/scala/org/sunbird/cloud/storage/exception/package.scala b/src/main/scala/org/sunbird/cloud/storage/exception/package.scala deleted file mode 100644 index 8606aab..0000000 --- a/src/main/scala/org/sunbird/cloud/storage/exception/package.scala +++ /dev/null @@ -1,7 +0,0 @@ -package org.sunbird.cloud.storage -/** - * Provides the Exception implementation for cloud-storage sdk. - * @since 1.0 - */ -package object exception { -} \ No newline at end of file diff --git a/src/main/scala/org/sunbird/cloud/storage/factory/StorageServiceFactory.scala b/src/main/scala/org/sunbird/cloud/storage/factory/StorageServiceFactory.scala deleted file mode 100644 index 4b2fd21..0000000 --- a/src/main/scala/org/sunbird/cloud/storage/factory/StorageServiceFactory.scala +++ /dev/null @@ -1,39 +0,0 @@ -package org.sunbird.cloud.storage.factory - -import org.sunbird.cloud.storage.BaseStorageService -import org.sunbird.cloud.storage.exception.StorageServiceException -import org.sunbird.cloud.storage.service.{AzureStorageService, CephS3StorageService, GcloudStorageService, OCIS3StorageService, S3StorageService, S3IAMStorageService} - -case class StorageConfig(`type`: String, storageKey: String, storageSecret: String, endPoint: Option[String] = None, region: Option[String] = Option(""), authType: Option[String] = Option("access_key")) - -object StorageServiceFactory { - - private def isIAMAuth(authType: Option[String]): Boolean = { - authType.exists(at => Set("iam", "iam_role", "instance_profile").contains(at.toLowerCase)) - } - - /** - * Creates StorageService based on the given config - * @param config - * @return - */ - def getStorageService(config: StorageConfig): BaseStorageService = { - config.`type`.toLowerCase() match { - case "aws" => - if (isIAMAuth(config.authType)) - new S3IAMStorageService(config) - else - new S3StorageService(config) - case "azure" => - new AzureStorageService(config); - case "cephs3" => - new CephS3StorageService(config); - case "gcloud" => - new GcloudStorageService(config); - case "oci" => - new OCIS3StorageService(config); - case _ => - throw new StorageServiceException("Unknown storage type found"); - } - } -} diff --git a/src/main/scala/org/sunbird/cloud/storage/factory/package.scala b/src/main/scala/org/sunbird/cloud/storage/factory/package.scala deleted file mode 100644 index 1791df4..0000000 --- a/src/main/scala/org/sunbird/cloud/storage/factory/package.scala +++ /dev/null @@ -1,7 +0,0 @@ -package org.sunbird.cloud.storage -/** - * Provides the factory implementation of StorageService. - * @since 1.0 - */ -package object factory { -} \ No newline at end of file diff --git a/src/main/scala/org/sunbird/cloud/storage/package.scala b/src/main/scala/org/sunbird/cloud/storage/package.scala deleted file mode 100644 index f79eb1c..0000000 --- a/src/main/scala/org/sunbird/cloud/storage/package.scala +++ /dev/null @@ -1,8 +0,0 @@ -package org.sunbird.cloud -/** -* Provides the interface and base implementation of StorageService. -* @since 1.0 -* -**/ -package object storage { -} \ No newline at end of file diff --git a/src/main/scala/org/sunbird/cloud/storage/service/AzureStorageService.scala b/src/main/scala/org/sunbird/cloud/storage/service/AzureStorageService.scala deleted file mode 100644 index 5fc428a..0000000 --- a/src/main/scala/org/sunbird/cloud/storage/service/AzureStorageService.scala +++ /dev/null @@ -1,17 +0,0 @@ -package org.sunbird.cloud.storage.service - -import org.jclouds.ContextBuilder -import org.jclouds.blobstore.BlobStoreContext -import org.sunbird.cloud.storage.BaseStorageService -import org.sunbird.cloud.storage.Model.Blob -import org.sunbird.cloud.storage.factory.StorageConfig - -class AzureStorageService(config: StorageConfig) extends BaseStorageService { - - var context = ContextBuilder.newBuilder("azureblob").credentials(config.storageKey, config.storageSecret).buildView(classOf[BlobStoreContext]) - var blobStore = context.getBlobStore - - override def getPaths(container: String, objects: List[Blob]): List[String] = { - objects.map{f => "wasb://" + container + "@" + config.storageKey + ".blob.core.windows.net/" + f.key} - } -} diff --git a/src/main/scala/org/sunbird/cloud/storage/service/CephS3StorageService.scala b/src/main/scala/org/sunbird/cloud/storage/service/CephS3StorageService.scala deleted file mode 100644 index 05d9976..0000000 --- a/src/main/scala/org/sunbird/cloud/storage/service/CephS3StorageService.scala +++ /dev/null @@ -1,19 +0,0 @@ -package org.sunbird.cloud.storage.service - -import org.jclouds.ContextBuilder -import org.jclouds.blobstore.BlobStoreContext -import org.sunbird.cloud.storage.BaseStorageService -import org.sunbird.cloud.storage.Model.Blob -import org.sunbird.cloud.storage.factory.StorageConfig - -import java.util.Properties - -class CephS3StorageService(config: StorageConfig) extends BaseStorageService { - - var context = ContextBuilder.newBuilder("s3").endpoint(config.endPoint.get).credentials(config.storageKey, config.storageSecret).buildView(classOf[BlobStoreContext]) - var blobStore = context.getBlobStore - - override def getPaths(container: String, objects: List[Blob]): List[String] = { - objects.map{f => "s3n://" + container + "/" + f.key} - } -} \ No newline at end of file diff --git a/src/main/scala/org/sunbird/cloud/storage/service/GcloudStorageService.scala b/src/main/scala/org/sunbird/cloud/storage/service/GcloudStorageService.scala deleted file mode 100644 index ffa23aa..0000000 --- a/src/main/scala/org/sunbird/cloud/storage/service/GcloudStorageService.scala +++ /dev/null @@ -1,334 +0,0 @@ -package org.sunbird.cloud.storage.service - -import com.google.auth.oauth2.ServiceAccountCredentials -import com.google.cloud.storage.{BlobId, BlobInfo, HttpMethod, Storage, StorageOptions} -import com.google.common.io.Files -import org.jclouds.ContextBuilder -import org.jclouds.blobstore.BlobStoreContext -import org.sunbird.cloud.storage.BaseStorageService -import org.sunbird.cloud.storage.Model.Blob -import org.sunbird.cloud.storage.exception.StorageServiceException -import org.sunbird.cloud.storage.factory.StorageConfig -import org.apache.tika.metadata.HttpHeaders -import org.apache.tika.mime.MimeTypes - -import java.io.{File, FileOutputStream} -import java.util.concurrent.TimeUnit -import scala.jdk.CollectionConverters._ - -class GcloudStorageService(config: StorageConfig) extends BaseStorageService { - - var context = ContextBuilder.newBuilder("google-cloud-storage").credentials(config.storageKey, config.storageSecret).buildView(classOf[BlobStoreContext]) - var blobStore = context.getBlobStore - - override def getPaths(container: String, objects: List[Blob]): List[String] = { - objects.map{f => "gs://" + container + "/" + f.key} - } - - // Overriding upload methos since multipart upload not working for GCP - override def upload(container: String, file: String, objectKey: String, isDirectory: Option[Boolean] = Option(false), attempt: Option[Int] = Option(1), retryCount: Option[Int] = None, ttl: Option[Int] = None): String = { - try { - if(isDirectory.get) { - val d = new File(file) - val files = filesList(d) - val list = files.map {f => - val key = objectKey + f.getAbsolutePath.split(d.getAbsolutePath + File.separator).last - upload(container, f.getAbsolutePath, key, Option(false), attempt, retryCount, ttl) - } - list.mkString(",") - } - else { - if (attempt.getOrElse(1) >= retryCount.getOrElse(maxRetries)) { - val message = s"Failed to upload. file: $file, key: $objectKey, attempt: $attempt, maxAttempts: $retryCount. Exceeded maximum number of retries" - throw new StorageServiceException(message) - } - - blobStore.createContainerInLocation(null, container) - val fileObj = new File(file) - val payload = Files.asByteSource(fileObj) - val contentType = tika.detect(fileObj) - val blob = blobStore.blobBuilder(objectKey).payload(payload).contentType(contentType).contentEncoding("UTF-8").contentLength(payload.size()).build() - blobStore.putBlob(container, blob) - if (ttl.isDefined) { - getPutSignedURL(container, objectKey, Option(ttl.get), None, Option(contentType)) - } else { - val host = "https://storage.googleapis.com/" - val name = blobStore.blobMetadata(container, objectKey).getName - host + container + "/" + name - } - } - } - catch { - case e: Exception => { - e.printStackTrace() - Thread.sleep(attempt.getOrElse(1)*2000) - val uploadAttempt = attempt.getOrElse(1) + 1 - if (uploadAttempt <= retryCount.getOrElse(maxRetries)) { - upload(container, file, objectKey, isDirectory, Option(uploadAttempt), retryCount, ttl) - } else { - throw e; - } - } - } - } - - override def getUri(container: String, _prefix: String, isDirectory: Option[Boolean] = Option(false)): String = { - val keys = listObjectKeys(container, _prefix); - if (keys.isEmpty) - throw new StorageServiceException("The given _prefix is incorrect: " + _prefix) - val prefix = keys.head - val blob = getObject(container, prefix, Option(false)) - val uri = blob.metadata.get("publicUri") - if (!uri.isEmpty) { - val host = "https://storage.googleapis.com/" - val name = blob.metadata.get("name").get.asInstanceOf[String] - host + container + "/" + name - } else - throw new StorageServiceException("uri not available for the given prefix: "+ _prefix) - } - /** - * Method to get V4 Signed URL when storage is GCP - * @param re - * @return - */ - override def getPutSignedURL(container: String, objectKey: String, ttl: Option[Int], permission: Option[String] = Option("r"), - contentType: Option[String] = Option("application/octet-stream"), additionalParams: Option[Map[String,String]] = None): String = { - if(additionalParams.isEmpty) { - throw new StorageServiceException("Missing google credentials params.") - } - // expiry time validation as TTL cannot be greater than 604800 - // expiry time will be set to default value of 604800 if greater than 604800 - val properties = additionalParams.get - // prepare common credentials, storage and blob info once - val credentials = ServiceAccountCredentials.fromPkcs8(properties.get("clientId").get, properties.get("clientEmail").get, properties.get("privateKeyPkcs8").get, - properties.get("privateKeyIds").get, new java.util.ArrayList[String]()) - val storage = StorageOptions.newBuilder.setProjectId(properties.get("projectId").get).setCredentials(credentials).build.getService - val blobInfo = BlobInfo.newBuilder(BlobId.of(container, objectKey)).build - val expiryTime = if(ttl.get > maxSignedurlTTL) maxSignedurlTTL else ttl.get - //creating signed url - val effectivePermission = permission.getOrElse("r").toLowerCase - val url = effectivePermission match { - case "w" => - // extract optional flag to indicate resumable/chunked upload, defaulting to false - val isChunkedUpload: Boolean = properties.get("chunked").map(_.toLowerCase).contains("true") - if (isChunkedUpload) { - // Only for chunked uploads, use resumable POST and include required headers - val extensionHeaders = Map(HttpHeaders.CONTENT_TYPE -> contentType.getOrElse(MimeTypes.OCTET_STREAM), "x-goog-resumable" -> "start") - storage.signUrl( - blobInfo, - expiryTime, - TimeUnit.SECONDS, - Storage.SignUrlOption.httpMethod(HttpMethod.POST), - Storage.SignUrlOption.withV4Signature, - Storage.SignUrlOption.withExtHeaders(extensionHeaders.asJava) - ) - } else { - // Original behavior: simple PUT without extra headers - storage.signUrl( - blobInfo, - expiryTime, - TimeUnit.SECONDS, - Storage.SignUrlOption.httpMethod(HttpMethod.PUT), - Storage.SignUrlOption.withV4Signature - ) - } - case _ => - // Default and for any invalid value: generate READ signed URL (GET) - storage.signUrl( - blobInfo, - expiryTime, - TimeUnit.SECONDS, - Storage.SignUrlOption.httpMethod(HttpMethod.GET), - Storage.SignUrlOption.withV4Signature - ) - } - url.toString; - } - - // Override listObjectKeys to avoid JClouds parsing issues - override def listObjectKeys(container: String, prefix: String): List[String] = { - try { - println(s"Listing objects with prefix '$prefix' in container '$container' using direct HTTP") - - // Use the Google Cloud Storage JSON API - val host = "https://storage.googleapis.com/storage/v1/b" - val url = new java.net.URL(s"${host}/${container}/o?prefix=${java.net.URLEncoder.encode(prefix, "UTF-8")}") - - println(s"Querying API URL: ${url}") - - val connection = url.openConnection().asInstanceOf[java.net.HttpURLConnection] - connection.setRequestMethod("GET") - connection.setRequestProperty("Accept", "application/json") - - val responseCode = connection.getResponseCode - if (responseCode != 200) { - println(s"Failed to list objects. Response code: $responseCode") - return List.empty[String] - } - - // Parse the JSON response - val responseStream = connection.getInputStream - val responseString = scala.io.Source.fromInputStream(responseStream).mkString - responseStream.close() - - // Parse items from the response - val items = parseJsonItems(responseString) - - if (items.isEmpty) { - println("No items found in the response") - List.empty[String] - } else { - println(s"Found ${items.size} objects") - items - } - } catch { - case e: Exception => - println(s"Error listing objects: ${e.getMessage}") - e.printStackTrace() - List.empty[String] - } - } - - // Helper method to parse JSON without external dependencies - private def parseJsonItems(json: String): List[String] = { - try { - // Simple regex-based JSON parsing to extract names - val namePattern = """"name"\s*:\s*"([^"]+)"""".r - val matches = namePattern.findAllMatchIn(json) - matches.map(_.group(1)).toList - } catch { - case e: Exception => - println(s"Error parsing JSON: ${e.getMessage}") - e.printStackTrace() - List.empty[String] - } - } - - override def download(container: String, objectKey: String, localPath: String, isDirectory: Option[Boolean] = Option(false)): Unit = { - try { - if(isDirectory.get) { - println(s"Starting GCP directory download operation for: container=$container, prefix=$objectKey to $localPath") - val objects = listObjectKeys(container, objectKey) - - if (objects.isEmpty) { - println(s"No objects found with prefix $objectKey in container $container") - return - } - - println(s"Found ${objects.size} objects to download from prefix $objectKey") - - // Ensure local directory exists - val downloadDir = new File(localPath) - if (!downloadDir.exists()) { - downloadDir.mkdirs() - } - - var successCount = 0 - var failureCount = 0 - - // Download each object - for (obj <- objects) { - try { - // Calculate the relative path from the prefix - val relativePath = if (obj.startsWith(objectKey)) { - obj.substring(objectKey.length) - } else { - obj - } - - // Create full local path with directories - val fullLocalPath = new File(localPath, relativePath) - val parentDir = fullLocalPath.getParentFile - if (parentDir != null && !parentDir.exists()) { - parentDir.mkdirs() - } - - println(s"Downloading object: $obj to ${fullLocalPath.getAbsolutePath}") - val result = downloadFile(container, obj, fullLocalPath.getAbsolutePath) - - if (result) { - successCount += 1 - } else { - failureCount += 1 - } - } catch { - case e: Exception => - println(s"Error downloading object $obj: ${e.getMessage}") - failureCount += 1 - } - } - - println(s"Directory download completed. Successfully downloaded $successCount files. Failed to download $failureCount files.") - } else { - println(s"Starting GCP file download operation for: container=$container, objectKey=$objectKey to $localPath") - - val fileName = objectKey.split("/").last - val downloadFilePath = new File(localPath, fileName).getAbsolutePath - - downloadFile(container, objectKey, downloadFilePath) - } - } catch { - case e: Exception => - println(s"Error during download: ${e.getMessage}") - e.printStackTrace() - throw new StorageServiceException(s"Failed to download object $objectKey from container $container: ${e.getMessage}", e) - } - } - - /** - * Downloads a single file from GCP storage - * - * @param container The storage container/bucket name - * @param objectKey The object key to download - * @param destinationPath The full path where the file should be saved - * @return true if download was successful, false otherwise - */ - private def downloadFile(container: String, objectKey: String, destinationPath: String): Boolean = { - try { - // Ensure parent directory exists - val downloadFile = new File(destinationPath) - val parentDir = downloadFile.getParentFile - if (parentDir != null && !parentDir.exists()) { - parentDir.mkdirs() - } - - // Handle the download using a direct HTTP approach to avoid JClouds parsing issues - val host = "https://storage.googleapis.com/" - val url = new java.net.URL(s"${host}${container}/${objectKey}") - - println(s"Downloading from URL: ${url}") - - val connection = url.openConnection().asInstanceOf[java.net.HttpURLConnection] - connection.setRequestMethod("GET") - - val responseCode = connection.getResponseCode - if (responseCode != 200) { - println(s"Failed to download $objectKey. Response code: $responseCode") - return false - } - - val inputStream = connection.getInputStream - val outputStream = new FileOutputStream(downloadFile) - - try { - val buffer = new Array[Byte](8192) - var bytesRead = inputStream.read(buffer) - while (bytesRead != -1) { - outputStream.write(buffer, 0, bytesRead) - bytesRead = inputStream.read(buffer) - } - println(s"Successfully downloaded file to ${downloadFile.getAbsolutePath}") - true - } finally { - if (inputStream != null) inputStream.close() - if (outputStream != null) outputStream.close() - } - } catch { - case e: Exception => - println(s"Error downloading object $objectKey: ${e.getMessage}") - e.printStackTrace() - false - } - } - -} \ No newline at end of file diff --git a/src/main/scala/org/sunbird/cloud/storage/service/OCIS3StorageService.scala b/src/main/scala/org/sunbird/cloud/storage/service/OCIS3StorageService.scala deleted file mode 100644 index b64d9e7..0000000 --- a/src/main/scala/org/sunbird/cloud/storage/service/OCIS3StorageService.scala +++ /dev/null @@ -1,59 +0,0 @@ -package org.sunbird.cloud.storage.service - -import com.google.common.io.Files -import com.google.common.hash -import org.jclouds.ContextBuilder -import org.jclouds.blobstore.BlobStoreContext - -import org.sunbird.cloud.storage.{BaseStorageService, Model} -import org.sunbird.cloud.storage.factory.StorageConfig - -import java.io.File -import java.util.Properties - -class OCIS3StorageService (config: StorageConfig) extends BaseStorageService { - - - val overrides = new Properties() - - overrides.setProperty("jclouds.provider", "s3") - overrides.setProperty("jclouds.endpoint", config.endPoint.get) - overrides.setProperty("jclouds.s3.virtual-host-buckets", "false") - overrides.setProperty("jclouds.strip-expect-header", "true") - overrides.setProperty("jclouds.regions", config.region.get) - overrides.setProperty("jclouds.s3.signer-version", "4") - - var context = ContextBuilder.newBuilder("aws-s3") - .credentials(config.storageKey, config.storageSecret) - .overrides(overrides) - .endpoint(config.endPoint.get).buildView(classOf[BlobStoreContext]) - var blobStore = context.getBlobStore - - /** - * Get HDFS compatible file paths to be used in tech stack like Spark. - * For ex: for S3 the file path is prefixed with s3n:/// and for Azure blob storage it would be wasbs://@.blob.core.windows.net// - * - * @param container String - The container/bucket of the objects - * @param objects List[Blob] - The Blob objects in the given container - * @return List[String] - HDFS compatible file paths. - */ - override def getPaths(container: String, objects: List[Model.Blob]): List[String] = { - objects.map{f => "s3n://" + container + "/" + f.key} - } - - override def createContainerInLocation(container: String): Unit = { - if (!( blobStore.containerExists(container))) { - blobStore.createContainerInLocation(null, container) - } - } - - override def putBlob(objectKey: String, file: File, container: String): Unit = { - val payload = Files.asByteSource(file) - val payloadSize = payload.size() - val payloadMD5 = payload.hash(hash.Hashing.md5()) - val contentType = tika.detect(file) - val blob = blobStore.blobBuilder(objectKey).payload(payload).contentType(contentType).contentEncoding("UTF-8").contentLength(payloadSize).contentMD5(payloadMD5).build() - blobStore.putBlob(container, blob) - } - -} diff --git a/src/main/scala/org/sunbird/cloud/storage/service/S3IAMStorageService.scala b/src/main/scala/org/sunbird/cloud/storage/service/S3IAMStorageService.scala deleted file mode 100644 index 01c0f54..0000000 --- a/src/main/scala/org/sunbird/cloud/storage/service/S3IAMStorageService.scala +++ /dev/null @@ -1,40 +0,0 @@ -package org.sunbird.cloud.storage.service - -import org.jclouds.ContextBuilder -import org.jclouds.blobstore.BlobStoreContext -import org.sunbird.cloud.storage.BaseStorageService -import org.sunbird.cloud.storage.Model.Blob -import org.sunbird.cloud.storage.factory.StorageConfig -import org.sunbird.cloud.storage.util.IAMCredentialsSupplier -import java.util.Properties - -/** - * S3 Storage Service that uses IAM Role-based authentication. - * Uses AWS SDK v2 DefaultCredentialsProvider to resolve credentials from: - * - EC2 Instance Profile - * - ECS Task Role - * - EKS IRSA (IAM Roles for Service Accounts) - * - Environment variables - * - AWS credentials file - */ -class S3IAMStorageService(config: StorageConfig) extends BaseStorageService { - - // Region configuration (recommended for IAM auth) - private val awsRegion = config.region.filter(_.nonEmpty).getOrElse("us-east-1") - - // Configure region via JClouds properties - private val overrides = new Properties() - overrides.setProperty("jclouds.regions", awsRegion) - - // Use credentialsSupplier for dynamic IAM credential resolution - var context = ContextBuilder.newBuilder("aws-s3") - .credentialsSupplier(new IAMCredentialsSupplier()) - .overrides(overrides) - .buildView(classOf[BlobStoreContext]) - - var blobStore = context.getBlobStore - - override def getPaths(container: String, objects: List[Blob]): List[String] = { - objects.map(f => "s3n://" + container + "/" + f.key) - } -} diff --git a/src/main/scala/org/sunbird/cloud/storage/service/S3StorageService.scala b/src/main/scala/org/sunbird/cloud/storage/service/S3StorageService.scala deleted file mode 100644 index 00260c7..0000000 --- a/src/main/scala/org/sunbird/cloud/storage/service/S3StorageService.scala +++ /dev/null @@ -1,17 +0,0 @@ -package org.sunbird.cloud.storage.service - -import org.jclouds.ContextBuilder -import org.jclouds.blobstore.BlobStoreContext -import org.sunbird.cloud.storage.BaseStorageService -import org.sunbird.cloud.storage.Model.Blob -import org.sunbird.cloud.storage.factory.StorageConfig - -class S3StorageService(config: StorageConfig) extends BaseStorageService { - - var context = ContextBuilder.newBuilder("aws-s3").credentials(config.storageKey, config.storageSecret).buildView(classOf[BlobStoreContext]) - var blobStore = context.getBlobStore - - override def getPaths(container: String, objects: List[Blob]): List[String] = { - objects.map{f => "s3n://" + container + "/" + f.key} - } -} diff --git a/src/main/scala/org/sunbird/cloud/storage/service/package.scala b/src/main/scala/org/sunbird/cloud/storage/service/package.scala deleted file mode 100644 index d660632..0000000 --- a/src/main/scala/org/sunbird/cloud/storage/service/package.scala +++ /dev/null @@ -1,8 +0,0 @@ -package org.sunbird.cloud.storage -/** -* Provides the implementation of StorageService. -* @since 1.0 -* -**/ -package object service { -} \ No newline at end of file diff --git a/src/main/scala/org/sunbird/cloud/storage/util/CommonUtil.scala b/src/main/scala/org/sunbird/cloud/storage/util/CommonUtil.scala deleted file mode 100644 index f91c8f0..0000000 --- a/src/main/scala/org/sunbird/cloud/storage/util/CommonUtil.scala +++ /dev/null @@ -1,98 +0,0 @@ -package org.sunbird.cloud.storage.util - -import org.joda.time.DateTime -import org.joda.time.DateTimeZone -import org.joda.time.Days -import org.joda.time.LocalDate -import org.joda.time.Weeks -import org.joda.time.Years -import org.joda.time.format.DateTimeFormat -import org.joda.time.format.DateTimeFormatter -import java.util.Date -import java.io.InputStream -import java.nio.file.Files -import java.nio.file.Paths -import java.nio.file.Paths.get -import java.nio.file.StandardCopyOption -import java.io.{File, FileInputStream, FileOutputStream, IOException} -import java.util.zip.{ZipEntry, ZipInputStream} - -import org.apache.commons.io.FilenameUtils -import org.apache.commons.lang3.StringUtils - -object CommonUtil { - - @transient val dateFormat: DateTimeFormatter = DateTimeFormat.forPattern("yyyy-MM-dd").withZoneUTC(); - - def getStartDate(endDate: Option[String], delta: Int): Option[String] = { - val to = if (endDate.nonEmpty) dateFormat.parseLocalDate(endDate.get) else LocalDate.fromDateFields(new Date); - Option(to.minusDays(delta).toString()); - } - - def getDatesBetween(fromDate: String, toDate: Option[String], pattern: String): Array[String] = { - val df: DateTimeFormatter = DateTimeFormat.forPattern(pattern).withZoneUTC(); - val to = if (toDate.nonEmpty) df.parseLocalDate(toDate.get) else LocalDate.fromDateFields(new Date); - val from = df.parseLocalDate(fromDate); - val dates = datesBetween(from, to); - dates.map { x => df.print(x) }.toArray; - } - - def getDatesBetween(fromDate: String, toDate: Option[String]): Array[String] = { - getDatesBetween(fromDate, toDate, "yyyy-MM-dd"); - } - - def datesBetween(from: LocalDate, to: LocalDate): IndexedSeq[LocalDate] = { - val numberOfDays = Days.daysBetween(from, to).getDays() - for (f <- 0 to numberOfDays) yield from.plusDays(f) - } - - def createDirectory(dir: String) { - val path = get(dir); - Files.createDirectories(path); - } - - def copyFile(from: InputStream, path: String, fileName: String) = { - createDirectory(path); - Files.copy(from, Paths.get(path + fileName), StandardCopyOption.REPLACE_EXISTING); - } - - def unZip(zipFile: String, outputFolder: String): Unit = { - val buffer = new Array[Byte](1024) - try { - //output directory - val folder = new File(outputFolder); - if (!folder.exists()) { - folder.mkdir(); - } - //zip file content - val zis: ZipInputStream = new ZipInputStream(new FileInputStream(zipFile)); - //get the zipped file list entry - var ze: ZipEntry = zis.getNextEntry(); - - while (ze != null) { - val fileName = ze.getName(); - if (!(FilenameUtils.getName(fileName).startsWith(".") | fileName.startsWith("_") | StringUtils.isBlank(FilenameUtils.getName(fileName)))) { - val newFile = new File(outputFolder + File.separator + fileName); - //create folders - new File(newFile.getParent()).mkdirs(); - val fos = new FileOutputStream(newFile); - var len: Int = zis.read(buffer); - while (len > 0) { - fos.write(buffer, 0, len) - len = zis.read(buffer) - } - fos.close() - } - ze = zis.getNextEntry() - } - zis.closeEntry() - zis.close() - - } catch { - case e: IOException => - e.printStackTrace() - println("exception caught: " + e.getMessage) - } - - } -} diff --git a/src/main/scala/org/sunbird/cloud/storage/util/IAMCredentialsSupplier.scala b/src/main/scala/org/sunbird/cloud/storage/util/IAMCredentialsSupplier.scala deleted file mode 100644 index 0005033..0000000 --- a/src/main/scala/org/sunbird/cloud/storage/util/IAMCredentialsSupplier.scala +++ /dev/null @@ -1,40 +0,0 @@ -package org.sunbird.cloud.storage.util - -import software.amazon.awssdk.auth.credentials.{DefaultCredentialsProvider, AwsCredentials, AwsSessionCredentials} -import org.jclouds.domain.Credentials -import org.jclouds.aws.domain.SessionCredentials -import com.google.common.base.Supplier - -/** - * A Guava Supplier that bridges AWS SDK v2 credential resolution with JClouds. - * Uses DefaultCredentialsProvider which checks credentials in this order: - * 1. System properties (aws.accessKeyId, aws.secretAccessKey) - * 2. Environment variables (AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY) - * 3. Web Identity Token (EKS IRSA) - * 4. Credential profiles (~/.aws/credentials) - * 5. ECS Container credentials - * 6. EC2 Instance Profile (IMDS) - * - * For IAM roles (EC2, ECS, EKS), temporary credentials with session tokens are returned. - */ -class IAMCredentialsSupplier extends Supplier[Credentials] { - - private val awsProvider = DefaultCredentialsProvider.builder().build() - - override def get(): Credentials = { - val awsCreds: AwsCredentials = awsProvider.resolveCredentials() - - awsCreds match { - case sessionCreds: AwsSessionCredentials => - // IAM role credentials include a session token that must be passed to JClouds - SessionCredentials.builder() - .accessKeyId(sessionCreds.accessKeyId()) - .secretAccessKey(sessionCreds.secretAccessKey()) - .sessionToken(sessionCreds.sessionToken()) - .build() - case _ => - // Static credentials (from env vars, config file, etc.) - new Credentials(awsCreds.accessKeyId(), awsCreds.secretAccessKey()) - } - } -} diff --git a/src/main/scala/org/sunbird/cloud/storage/util/JSONUtils.scala b/src/main/scala/org/sunbird/cloud/storage/util/JSONUtils.scala deleted file mode 100644 index 364d228..0000000 --- a/src/main/scala/org/sunbird/cloud/storage/util/JSONUtils.scala +++ /dev/null @@ -1,48 +0,0 @@ -package org.sunbird.cloud.storage.util - -import java.lang.reflect.ParameterizedType -import java.lang.reflect.Type -import com.fasterxml.jackson.core.`type`.TypeReference -import com.fasterxml.jackson.databind.DeserializationFeature -import com.fasterxml.jackson.databind.ObjectMapper -import com.fasterxml.jackson.module.scala.DefaultScalaModule -import com.fasterxml.jackson.annotation.JsonInclude -import com.fasterxml.jackson.annotation.JsonInclude.Include -import com.fasterxml.jackson.annotation.JsonInclude.Value -import com.fasterxml.jackson.databind.SerializationFeature - - -object JSONUtils { - - @transient val mapper = new ObjectMapper(); - mapper.registerModule(DefaultScalaModule); - mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); - mapper.configOverride(Map.getClass).setInclude(Value.construct(JsonInclude.Include.NON_NULL, JsonInclude.Include.NON_NULL)) - mapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false); - mapper.setSerializationInclusion(Include.NON_NULL); - - @throws(classOf[Exception]) - def serialize(obj: AnyRef): String = { - mapper.writeValueAsString(obj); - } - - @throws(classOf[Exception]) - def deserialize[T: Manifest](value: String): T = mapper.readValue(value, typeReference[T]); - - private[this] def typeReference[T: Manifest] = new TypeReference[T] { - override def getType = typeFromManifest(manifest[T]) - } - - - private[this] def typeFromManifest(m: Manifest[_]): Type = { - if (m.typeArguments.isEmpty) { m.runtimeClass } - // $COVERAGE-OFF$Disabling scoverage as this code is impossible to test - else new ParameterizedType { - def getRawType = m.runtimeClass - def getActualTypeArguments = m.typeArguments.map(typeFromManifest).toArray - def getOwnerType = null - } - // $COVERAGE-ON$ - } - -} diff --git a/src/main/scala/org/sunbird/cloud/storage/util/package.scala b/src/main/scala/org/sunbird/cloud/storage/util/package.scala deleted file mode 100644 index b835b9d..0000000 --- a/src/main/scala/org/sunbird/cloud/storage/util/package.scala +++ /dev/null @@ -1,7 +0,0 @@ -package org.sunbird.cloud.storage -/** - * Provides utility APIs. - * @since 1.0 - */ -package object util { -} \ No newline at end of file diff --git a/src/test/resources/1234/part-00000 b/src/test/resources/1234/part-00000 deleted file mode 100644 index 40bc409..0000000 --- a/src/test/resources/1234/part-00000 +++ /dev/null @@ -1,286 +0,0 @@ -{"eid":"OE_INTERACT","ts":"2016-05-05T11:13:04.305+0530","ets":1462426984305,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.haircut","ver":"1"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"splash:cover_sound","stageid":"splash","uri":"","subtype":"PLAY","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:28+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.699Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:13:11.891+0530","ets":1462426991891,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.haircut","ver":"1"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"TOUCH","extype":"","id":"next","stageid":"splash","uri":"","subtype":"","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:29+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.699Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:13:12.103+0530","ets":1462426992103,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.haircut","ver":"1"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"scene1:scene1_sound","stageid":"scene1","uri":"","subtype":"PLAY","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:29+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.699Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:13:14.053+0530","ets":1462426994053,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.haircut","ver":"1"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"TOUCH","extype":"","id":"next","stageid":"scene1","uri":"","subtype":"","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:29+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.699Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:13:14.260+0530","ets":1462426994260,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.haircut","ver":"1"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"scene2:scene2_sound","stageid":"scene2","uri":"","subtype":"PLAY","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:29+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.699Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:13:14.270+0530","ets":1462426994270,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.haircut","ver":"1"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"scene1:scene1_sound","stageid":"scene2","uri":"","subtype":"STOP","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:29+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.699Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:13:15.241+0530","ets":1462426995241,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.haircut","ver":"1"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"TOUCH","extype":"","id":"next","stageid":"scene2","uri":"","subtype":"","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:29+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.700Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:13:15.412+0530","ets":1462426995412,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.haircut","ver":"1"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"scene3:scene3_sound","stageid":"scene3","uri":"","subtype":"PLAY","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:29+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.700Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:13:15.424+0530","ets":1462426995424,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.haircut","ver":"1"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"scene2:scene2_sound","stageid":"scene3","uri":"","subtype":"STOP","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:29+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.700Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:13:16.040+0530","ets":1462426996040,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.haircut","ver":"1"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"TOUCH","extype":"","id":"next","stageid":"scene3","uri":"","subtype":"","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:29+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.700Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:13:16.190+0530","ets":1462426996190,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.haircut","ver":"1"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"scene4:scene4_sound","stageid":"scene4","uri":"","subtype":"PLAY","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:29+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.700Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:13:16.198+0530","ets":1462426996198,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.haircut","ver":"1"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"scene3:scene3_sound","stageid":"scene4","uri":"","subtype":"STOP","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:29+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.700Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:13:16.973+0530","ets":1462426996973,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.haircut","ver":"1"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"TOUCH","extype":"","id":"next","stageid":"scene4","uri":"","subtype":"","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:29+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.700Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:13:17.308+0530","ets":1462426997308,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.haircut","ver":"1"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"scene5:scene5_sound","stageid":"scene5","uri":"","subtype":"PLAY","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:29+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.701Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:13:17.318+0530","ets":1462426997318,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.haircut","ver":"1"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"scene4:scene4_sound","stageid":"scene5","uri":"","subtype":"STOP","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:29+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.701Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:13:18.028+0530","ets":1462426998028,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.haircut","ver":"1"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"TOUCH","extype":"","id":"next","stageid":"scene5","uri":"","subtype":"","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:29+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.701Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:13:18.175+0530","ets":1462426998175,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.haircut","ver":"1"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"scene6:scene6_sound","stageid":"scene6","uri":"","subtype":"PLAY","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:29+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.701Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:13:18.185+0530","ets":1462426998185,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.haircut","ver":"1"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"scene5:scene5_sound","stageid":"scene6","uri":"","subtype":"STOP","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:29+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.701Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:13:18.918+0530","ets":1462426998918,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.haircut","ver":"1"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"TOUCH","extype":"","id":"next","stageid":"scene6","uri":"","subtype":"","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:29+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.701Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:13:19.091+0530","ets":1462426999091,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.haircut","ver":"1"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"scene7:scene7_sound","stageid":"scene7","uri":"","subtype":"PLAY","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:29+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.701Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:13:19.099+0530","ets":1462426999099,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.haircut","ver":"1"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"scene6:scene6_sound","stageid":"scene7","uri":"","subtype":"STOP","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:29+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.701Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:13:19.864+0530","ets":1462426999864,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.haircut","ver":"1"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"TOUCH","extype":"","id":"next","stageid":"scene7","uri":"","subtype":"","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:29+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.702Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:13:20.044+0530","ets":1462427000044,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.haircut","ver":"1"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"scene8:scene8_sound","stageid":"scene8","uri":"","subtype":"PLAY","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:29+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.702Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:13:20.050+0530","ets":1462427000050,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.haircut","ver":"1"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"scene7:scene7_sound","stageid":"scene8","uri":"","subtype":"STOP","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:29+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.702Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:13:23.734+0530","ets":1462427003734,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.haircut","ver":"1"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"TOUCH","extype":"","id":"next","stageid":"scene8","uri":"","subtype":"","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:29+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.702Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:13:23.897+0530","ets":1462427003897,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.haircut","ver":"1"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"scene9:scene9_sound","stageid":"scene9","uri":"","subtype":"PLAY","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:29+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.702Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:13:23.908+0530","ets":1462427003908,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.haircut","ver":"1"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"scene8:scene8_sound","stageid":"scene9","uri":"","subtype":"STOP","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:29+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.702Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:13:24.755+0530","ets":1462427004755,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.haircut","ver":"1"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"TOUCH","extype":"","id":"next","stageid":"scene9","uri":"","subtype":"","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:29+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.702Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:13:24.923+0530","ets":1462427004923,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.haircut","ver":"1"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"scene10:scene10_sound","stageid":"scene10","uri":"","subtype":"PLAY","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:29+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.703Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:13:24.932+0530","ets":1462427004932,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.haircut","ver":"1"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"scene9:scene9_sound","stageid":"scene10","uri":"","subtype":"STOP","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:29+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.703Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:13:25.661+0530","ets":1462427005661,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.haircut","ver":"1"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"TOUCH","extype":"","id":"next","stageid":"scene10","uri":"","subtype":"","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:29+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.703Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:13:25.827+0530","ets":1462427005827,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.haircut","ver":"1"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"scene11:scene11_sound","stageid":"scene11","uri":"","subtype":"PLAY","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:29+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.703Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:13:25.836+0530","ets":1462427005836,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.haircut","ver":"1"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"scene10:scene10_sound","stageid":"scene11","uri":"","subtype":"STOP","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:29+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.703Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:13:26.637+0530","ets":1462427006637,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.haircut","ver":"1"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"TOUCH","extype":"","id":"next","stageid":"scene11","uri":"","subtype":"","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:29+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.703Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:13:26.795+0530","ets":1462427006795,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.haircut","ver":"1"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"scene12:scene12_sound","stageid":"scene12","uri":"","subtype":"PLAY","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:29+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.703Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:13:26.809+0530","ets":1462427006809,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.haircut","ver":"1"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"scene11:scene11_sound","stageid":"scene12","uri":"","subtype":"STOP","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:29+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.703Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:13:27.501+0530","ets":1462427007501,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.haircut","ver":"1"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"TOUCH","extype":"","id":"next","stageid":"scene12","uri":"","subtype":"","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:29+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.704Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:13:27.630+0530","ets":1462427007630,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.haircut","ver":"1"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"scene13:scene13_sound","stageid":"scene13","uri":"","subtype":"PLAY","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:29+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.704Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:13:27.639+0530","ets":1462427007639,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.haircut","ver":"1"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"scene12:scene12_sound","stageid":"scene13","uri":"","subtype":"STOP","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:29+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.704Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:13:28.309+0530","ets":1462427008309,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.haircut","ver":"1"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"TOUCH","extype":"","id":"next","stageid":"scene13","uri":"","subtype":"","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:29+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.704Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:13:28.439+0530","ets":1462427008439,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.haircut","ver":"1"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"scene14:scene14_sound","stageid":"scene14","uri":"","subtype":"PLAY","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:29+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.704Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:13:28.445+0530","ets":1462427008445,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.haircut","ver":"1"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"scene13:scene13_sound","stageid":"scene14","uri":"","subtype":"STOP","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:29+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.704Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:13:29.091+0530","ets":1462427009091,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.haircut","ver":"1"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"TOUCH","extype":"","id":"next","stageid":"scene14","uri":"","subtype":"","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:29+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.705Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:13:29.238+0530","ets":1462427009238,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.haircut","ver":"1"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"scene15:scene15_sound","stageid":"scene15","uri":"","subtype":"PLAY","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:29+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.705Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:13:29.245+0530","ets":1462427009245,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.haircut","ver":"1"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"scene14:scene14_sound","stageid":"scene15","uri":"","subtype":"STOP","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:29+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.705Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:13:30.026+0530","ets":1462427010026,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.haircut","ver":"1"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"TOUCH","extype":"","id":"next","stageid":"scene15","uri":"","subtype":"","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:29+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.705Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:13:30.198+0530","ets":1462427010198,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.haircut","ver":"1"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"scene16:scene16_sound","stageid":"scene16","uri":"","subtype":"PLAY","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:29+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.705Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:13:30.206+0530","ets":1462427010206,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.haircut","ver":"1"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"scene15:scene15_sound","stageid":"scene16","uri":"","subtype":"STOP","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:29+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.705Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:13:30.989+0530","ets":1462427010989,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.haircut","ver":"1"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"TOUCH","extype":"","id":"next","stageid":"scene16","uri":"","subtype":"","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:29+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.705Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:13:31.142+0530","ets":1462427011142,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.haircut","ver":"1"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"scene17:scene17_sound","stageid":"scene17","uri":"","subtype":"PLAY","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:29+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.706Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:13:31.149+0530","ets":1462427011149,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.haircut","ver":"1"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"scene16:scene16_sound","stageid":"scene17","uri":"","subtype":"STOP","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:29+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.706Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:13:31.864+0530","ets":1462427011864,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.haircut","ver":"1"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"TOUCH","extype":"","id":"next","stageid":"scene17","uri":"","subtype":"","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:29+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.706Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:13:32.025+0530","ets":1462427012025,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.haircut","ver":"1"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"scene18:scene18_sound","stageid":"scene18","uri":"","subtype":"PLAY","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:29+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.706Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:13:32.031+0530","ets":1462427012032,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.haircut","ver":"1"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"scene17:scene17_sound","stageid":"scene18","uri":"","subtype":"STOP","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:29+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.706Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:13:34.111+0530","ets":1462427014111,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.haircut","ver":"1"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"TOUCH","extype":"","id":"next","stageid":"scene18","uri":"","subtype":"","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:29+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.706Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:13:34.255+0530","ets":1462427014255,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.haircut","ver":"1"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"scene19:scene19_sound","stageid":"scene19","uri":"","subtype":"PLAY","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:29+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.706Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:13:34.265+0530","ets":1462427014265,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.haircut","ver":"1"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"scene18:scene18_sound","stageid":"scene19","uri":"","subtype":"STOP","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:29+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.707Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:13:35.128+0530","ets":1462427015128,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.haircut","ver":"1"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"TOUCH","extype":"","id":"next","stageid":"scene19","uri":"","subtype":"","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:29+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.707Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:13:35.317+0530","ets":1462427015317,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.haircut","ver":"1"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"scene20:learning10_sound","stageid":"scene20","uri":"","subtype":"PLAY","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:29+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.707Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:13:35.323+0530","ets":1462427015323,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.haircut","ver":"1"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"scene19:scene19_sound","stageid":"scene20","uri":"","subtype":"STOP","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:29+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.707Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:13:43.122+0530","ets":1462427023122,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.haircut","ver":"1"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"TOUCH","extype":"","id":"next","stageid":"scene20","uri":"","subtype":"","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:29+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.707Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:13:44.703+0530","ets":1462427024703,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.haircut","ver":"1"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"TOUCH","extype":"","id":"next","stageid":"scene20","uri":"","subtype":"","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:29+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.707Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:13:45.697+0530","ets":1462427025697,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.haircut","ver":"1"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"TOUCH","extype":"","id":"next","stageid":"scene20","uri":"","subtype":"","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:29+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.707Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:13:45.864+0530","ets":1462427025864,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.haircut","ver":"1"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"scene20:learning12_sound","stageid":"scene20","uri":"","subtype":"PLAY","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:29+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.708Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:13:47.502+0530","ets":1462427027502,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.haircut","ver":"1"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"TOUCH","extype":"","id":"next","stageid":"scene20","uri":"","subtype":"","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:29+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.708Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:13:48.091+0530","ets":1462427028091,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.haircut","ver":"1"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"TOUCH","extype":"","id":"next","stageid":"scene21","uri":"","subtype":"","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:29+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.708Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:13:55.898+0530","ets":1462427035898,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.haircut","ver":"1"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"splash:cover_sound","stageid":"splash","uri":"","subtype":"PLAY","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:29+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.708Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:13:59.441+0530","ets":1462427039441,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.haircut","ver":"1"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"splash:cover_sound","stageid":"splash","uri":"","subtype":"STOP","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:29+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.709Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:13:59.918+0530","ets":1462427039918,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.haircut","ver":"1"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"splash:cover_sound","stageid":"splash","uri":"","subtype":"PLAY","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:29+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.709Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:14:05.353+0530","ets":1462427045353,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.haircut","ver":"1"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"splash:cover_sound","stageid":"splash","uri":"","subtype":"STOP","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:29+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.709Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:14:41.878+0530","ets":1462427081878,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.haircut","ver":"1"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"splash:cover_sound","stageid":"splash","uri":"","subtype":"PLAY","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:29+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.710Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:14:48.214+0530","ets":1462427088214,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.haircut","ver":"1"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"TOUCH","extype":"","id":"splash_audio","stageid":"splash","uri":"","subtype":"","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:29+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.710Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:14:48.258+0530","ets":1462427088258,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.haircut","ver":"1"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"splash:splash_audio","stageid":"splash","uri":"","subtype":"PLAY","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:29+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.710Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:14:50.279+0530","ets":1462427090279,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.haircut","ver":"1"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"TOUCH","extype":"","id":"previousContainer","stageid":"splash","uri":"","subtype":"","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:29+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.710Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:15:32.743+0530","ets":1462427132743,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.joy","ver":"1"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"splash:scene1_audio","stageid":"splash","uri":"","subtype":"PLAY","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:29+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.711Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:15:36.731+0530","ets":1462427136731,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.joy","ver":"1"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"TOUCH","extype":"","id":"next","stageid":"splash","uri":"","subtype":"","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:29+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.711Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:15:37.053+0530","ets":1462427137053,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.joy","ver":"1"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"scene2:scene2_audio","stageid":"scene2","uri":"","subtype":"PLAY","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:29+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.711Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:15:37.068+0530","ets":1462427137068,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.joy","ver":"1"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"splash:scene1_audio","stageid":"scene2","uri":"","subtype":"STOP","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:29+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.711Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:15:47.194+0530","ets":1462427147194,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.joy","ver":"1"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"TOUCH","extype":"","id":"previous","stageid":"scene2","uri":"","subtype":"","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:29+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.711Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:15:47.351+0530","ets":1462427147351,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.joy","ver":"1"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"splash:scene1_audio","stageid":"splash","uri":"","subtype":"PLAY","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:29+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.712Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:15:47.360+0530","ets":1462427147360,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.joy","ver":"1"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"scene2:scene2_audio","stageid":"splash","uri":"","subtype":"STOP","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:29+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.712Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:15:49.287+0530","ets":1462427149287,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.joy","ver":"1"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"TOUCH","extype":"","id":"next","stageid":"splash","uri":"","subtype":"","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:29+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.712Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:15:49.416+0530","ets":1462427149416,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.joy","ver":"1"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"scene2:scene2_audio","stageid":"scene2","uri":"","subtype":"PLAY","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:29+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.712Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:15:49.426+0530","ets":1462427149426,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.joy","ver":"1"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"splash:scene1_audio","stageid":"scene2","uri":"","subtype":"STOP","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:29+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.712Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:16:09.959+0530","ets":1462427169959,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.joy","ver":"1"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"scene2:scene2_audio","stageid":"scene2","uri":"","subtype":"PLAY","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:29+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.712Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:16:10.067+0530","ets":1462427170067,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.joy","ver":"1"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"scene2:scene2_audio","stageid":"scene2","uri":"","subtype":"STOP","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:29+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.712Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:16:10.488+0530","ets":1462427170488,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.joy","ver":"1"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"scene2:scene2_audio","stageid":"scene2","uri":"","subtype":"PLAY","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:29+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.713Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:16:11.989+0530","ets":1462427171989,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.joy","ver":"1"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"scene2:scene2_audio","stageid":"scene2","uri":"","subtype":"STOP","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:29+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.713Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:16:12.469+0530","ets":1462427172469,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.joy","ver":"1"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"scene2:scene2_audio","stageid":"scene2","uri":"","subtype":"PLAY","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:29+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.713Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:16:12.609+0530","ets":1462427172609,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.joy","ver":"1"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"scene2:scene2_audio","stageid":"scene2","uri":"","subtype":"STOP","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:29+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.713Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:16:13.070+0530","ets":1462427173070,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.joy","ver":"1"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"scene2:scene2_audio","stageid":"scene2","uri":"","subtype":"PLAY","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:29+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.713Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:16:13.380+0530","ets":1462427173380,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.joy","ver":"1"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"TOUCH","extype":"","id":"previous","stageid":"scene2","uri":"","subtype":"","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:29+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.713Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:16:13.504+0530","ets":1462427173504,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.joy","ver":"1"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"splash:scene1_audio","stageid":"splash","uri":"","subtype":"PLAY","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:29+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.713Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:16:13.519+0530","ets":1462427173519,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.joy","ver":"1"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"scene2:scene2_audio","stageid":"splash","uri":"","subtype":"STOP","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:29+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.713Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:16:15.040+0530","ets":1462427175040,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.joy","ver":"1"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"TOUCH","extype":"","id":"scene1_audio","stageid":"splash","uri":"","subtype":"","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:29+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.714Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:16:15.058+0530","ets":1462427175058,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.joy","ver":"1"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"splash:scene1_audio","stageid":"splash","uri":"","subtype":"PAUSE","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:29+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.714Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:16:17.189+0530","ets":1462427177189,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.joy","ver":"1"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"TOUCH","extype":"","id":"next","stageid":"splash","uri":"","subtype":"","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:29+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.714Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:16:17.353+0530","ets":1462427177353,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.joy","ver":"1"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"scene2:scene2_audio","stageid":"scene2","uri":"","subtype":"PLAY","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:29+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.714Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:16:17.370+0530","ets":1462427177370,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.joy","ver":"1"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"splash:scene1_audio","stageid":"scene2","uri":"","subtype":"STOP","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:29+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.714Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:16:34.007+0530","ets":1462427194007,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.joy","ver":"1"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"scene2:scene2_audio","stageid":"scene2","uri":"","subtype":"PLAY","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:29+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.714Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:16:34.019+0530","ets":1462427194019,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.joy","ver":"1"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"splash:scene1_audio","stageid":"scene2","uri":"","subtype":"STOP","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:29+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.714Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:16:34.580+0530","ets":1462427194580,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.joy","ver":"1"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"TOUCH","extype":"","id":"previous","stageid":"scene2","uri":"","subtype":"","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:29+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.714Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:16:34.750+0530","ets":1462427194750,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.joy","ver":"1"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"splash:scene1_audio","stageid":"splash","uri":"","subtype":"PLAY","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:29+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.714Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:16:34.758+0530","ets":1462427194758,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.joy","ver":"1"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"scene2:scene2_audio","stageid":"splash","uri":"","subtype":"STOP","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:29+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.715Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:16:36.072+0530","ets":1462427196072,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.joy","ver":"1"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"TOUCH","extype":"","id":"scene1_audio","stageid":"splash","uri":"","subtype":"","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:29+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.715Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:16:36.081+0530","ets":1462427196081,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.joy","ver":"1"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"splash:scene1_audio","stageid":"splash","uri":"","subtype":"PAUSE","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:29+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.715Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:16:37.581+0530","ets":1462427197581,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.joy","ver":"1"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"splash:scene1_audio","stageid":"splash","uri":"","subtype":"STOP","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:29+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.715Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:16:37.930+0530","ets":1462427197930,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.joy","ver":"1"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"splash:scene1_audio","stageid":"splash","uri":"","subtype":"STOP","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:29+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.715Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:16:38.174+0530","ets":1462427198174,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.joy","ver":"1"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"splash:scene1_audio","stageid":"splash","uri":"","subtype":"PLAY","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:29+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.715Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:16:39.711+0530","ets":1462427199711,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.joy","ver":"1"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"splash:scene1_audio","stageid":"splash","uri":"","subtype":"STOP","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:29+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.715Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:16:40.171+0530","ets":1462427200171,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.joy","ver":"1"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"splash:scene1_audio","stageid":"splash","uri":"","subtype":"PLAY","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:29+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.715Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:16:45.760+0530","ets":1462427205760,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.joy","ver":"1"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"TOUCH","extype":"","id":"next","stageid":"splash","uri":"","subtype":"","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:29+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.715Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:16:45.940+0530","ets":1462427205940,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.joy","ver":"1"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"TOUCH","extype":"","id":"previousContainer","stageid":"scene2","uri":"","subtype":"","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:29+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.716Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:16:46.132+0530","ets":1462427206132,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.joy","ver":"1"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"scene2:scene2_audio","stageid":"scene2","uri":"","subtype":"PLAY","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:29+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.716Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:16:46.145+0530","ets":1462427206145,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.joy","ver":"1"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"splash:scene1_audio","stageid":"scene2","uri":"","subtype":"STOP","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:29+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.716Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:17:01.281+0530","ets":1462427221281,"ver":"2.0","gdata":{"id":"org.ekstep.lit.moon_cap.story","ver":"12"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"splash:splash_sound","stageid":"splash","uri":"","subtype":"PLAY","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:29+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.717Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:17:01.326+0530","ets":1462427221326,"ver":"2.0","gdata":{"id":"org.ekstep.lit.moon_cap.story","ver":"12"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"splash:delayed_intro_sound","stageid":"splash","uri":"","subtype":"PLAY","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:29+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.717Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:17:07.760+0530","ets":1462427227760,"ver":"2.0","gdata":{"id":"org.ekstep.lit.moon_cap.story","ver":"12"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"TOUCH","extype":"","id":"next","stageid":"splash","uri":"","subtype":"","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:29+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.717Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:17:07.977+0530","ets":1462427227977,"ver":"2.0","gdata":{"id":"org.ekstep.lit.moon_cap.story","ver":"12"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"scene1:hs_s1_dial_sound","stageid":"scene1","uri":"","subtype":"PLAY","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:29+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.717Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:17:08.106+0530","ets":1462427228107,"ver":"2.0","gdata":{"id":"org.ekstep.lit.moon_cap.story","ver":"12"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"scene1:fair_sound","stageid":"scene1","uri":"","subtype":"PLAY","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:29+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.717Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:17:08.118+0530","ets":1462427228118,"ver":"2.0","gdata":{"id":"org.ekstep.lit.moon_cap.story","ver":"12"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"splash:delayed_intro_sound","stageid":"scene1","uri":"","subtype":"STOP","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:29+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.717Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:17:13.633+0530","ets":1462427233633,"ver":"2.0","gdata":{"id":"org.ekstep.lit.moon_cap.story","ver":"12"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"TOUCH","extype":"","id":"next","stageid":"scene1","uri":"","subtype":"","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:29+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.718Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:17:13.851+0530","ets":1462427233851,"ver":"2.0","gdata":{"id":"org.ekstep.lit.moon_cap.story","ver":"12"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"scene2:scene2_sound","stageid":"scene2","uri":"","subtype":"PLAY","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:29+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.718Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:17:13.859+0530","ets":1462427233859,"ver":"2.0","gdata":{"id":"org.ekstep.lit.moon_cap.story","ver":"12"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"scene1:fair_sound","stageid":"scene2","uri":"","subtype":"STOP","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:29+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.718Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:17:17.386+0530","ets":1462427237386,"ver":"2.0","gdata":{"id":"org.ekstep.lit.moon_cap.story","ver":"12"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"TOUCH","extype":"","id":"next","stageid":"scene2","uri":"","subtype":"","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:29+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.718Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:17:17.631+0530","ets":1462427237631,"ver":"2.0","gdata":{"id":"org.ekstep.lit.moon_cap.story","ver":"12"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"scene3:scene3_sound","stageid":"scene3","uri":"","subtype":"PLAY","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:29+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.718Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:17:17.696+0530","ets":1462427237696,"ver":"2.0","gdata":{"id":"org.ekstep.lit.moon_cap.story","ver":"12"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"scene3:wind_sound","stageid":"scene3","uri":"","subtype":"PLAY","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:29+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.718Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:17:17.700+0530","ets":1462427237700,"ver":"2.0","gdata":{"id":"org.ekstep.lit.moon_cap.story","ver":"12"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"HIDE","extype":"","id":"flying_cap1","stageid":"scene3","uri":"","subtype":"","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:29+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.718Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:17:17.704+0530","ets":1462427237704,"ver":"2.0","gdata":{"id":"org.ekstep.lit.moon_cap.story","ver":"12"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"SHOW","extype":"","id":"flying_cap2","stageid":"scene3","uri":"","subtype":"","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:29+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.718Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:17:17.726+0530","ets":1462427237726,"ver":"2.0","gdata":{"id":"org.ekstep.lit.moon_cap.story","ver":"12"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"scene2:scene2_sound","stageid":"scene3","uri":"","subtype":"STOP","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:30+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.718Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:17:21.753+0530","ets":1462427241753,"ver":"2.0","gdata":{"id":"org.ekstep.lit.moon_cap.story","ver":"12"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"TOUCH","extype":"","id":"previous","stageid":"scene3","uri":"","subtype":"","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:30+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.719Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:17:21.927+0530","ets":1462427241927,"ver":"2.0","gdata":{"id":"org.ekstep.lit.moon_cap.story","ver":"12"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"scene2:scene2_sound","stageid":"scene2","uri":"","subtype":"PLAY","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:30+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.719Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:17:21.935+0530","ets":1462427241935,"ver":"2.0","gdata":{"id":"org.ekstep.lit.moon_cap.story","ver":"12"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"scene3:scene3_sound","stageid":"scene2","uri":"","subtype":"STOP","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:30+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.719Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:17:29.392+0530","ets":1462427249392,"ver":"2.0","gdata":{"id":"org.ekstep.lit.moon_cap.story","ver":"12"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"TOUCH","extype":"","id":"next","stageid":"scene2","uri":"","subtype":"","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:30+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.719Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:17:29.545+0530","ets":1462427249545,"ver":"2.0","gdata":{"id":"org.ekstep.lit.moon_cap.story","ver":"12"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"scene3:scene3_sound","stageid":"scene3","uri":"","subtype":"PLAY","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:30+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.719Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:17:29.561+0530","ets":1462427249561,"ver":"2.0","gdata":{"id":"org.ekstep.lit.moon_cap.story","ver":"12"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"scene3:wind_sound","stageid":"scene3","uri":"","subtype":"PLAY","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:30+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.719Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:17:29.565+0530","ets":1462427249565,"ver":"2.0","gdata":{"id":"org.ekstep.lit.moon_cap.story","ver":"12"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"HIDE","extype":"","id":"flying_cap1","stageid":"scene3","uri":"","subtype":"","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:30+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.719Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:17:29.569+0530","ets":1462427249569,"ver":"2.0","gdata":{"id":"org.ekstep.lit.moon_cap.story","ver":"12"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"SHOW","extype":"","id":"flying_cap2","stageid":"scene3","uri":"","subtype":"","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:30+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.719Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:17:29.584+0530","ets":1462427249584,"ver":"2.0","gdata":{"id":"org.ekstep.lit.moon_cap.story","ver":"12"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"scene2:scene2_sound","stageid":"scene3","uri":"","subtype":"STOP","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:30+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.720Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:17:30.785+0530","ets":1462427250785,"ver":"2.0","gdata":{"id":"org.ekstep.lit.moon_cap.story","ver":"12"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"TOUCH","extype":"","id":"previous","stageid":"scene3","uri":"","subtype":"","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:30+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.720Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:17:30.964+0530","ets":1462427250964,"ver":"2.0","gdata":{"id":"org.ekstep.lit.moon_cap.story","ver":"12"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"scene2:scene2_sound","stageid":"scene2","uri":"","subtype":"PLAY","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:30+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.720Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:17:30.974+0530","ets":1462427250974,"ver":"2.0","gdata":{"id":"org.ekstep.lit.moon_cap.story","ver":"12"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"scene3:scene3_sound","stageid":"scene2","uri":"","subtype":"STOP","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:30+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.720Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:17:30.981+0530","ets":1462427250981,"ver":"2.0","gdata":{"id":"org.ekstep.lit.moon_cap.story","ver":"12"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"scene3:wind_sound","stageid":"scene2","uri":"","subtype":"STOP","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:30+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.720Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:17:40.745+0530","ets":1462427260745,"ver":"2.0","gdata":{"id":"org.ekstep.lit.moon_cap.story","ver":"12"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"TOUCH","extype":"","id":"next","stageid":"scene2","uri":"","subtype":"","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:30+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.720Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:17:40.937+0530","ets":1462427260937,"ver":"2.0","gdata":{"id":"org.ekstep.lit.moon_cap.story","ver":"12"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"scene3:scene3_sound","stageid":"scene3","uri":"","subtype":"PLAY","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:30+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.720Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:17:40.957+0530","ets":1462427260957,"ver":"2.0","gdata":{"id":"org.ekstep.lit.moon_cap.story","ver":"12"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"scene3:wind_sound","stageid":"scene3","uri":"","subtype":"PLAY","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:30+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.720Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:17:40.994+0530","ets":1462427260994,"ver":"2.0","gdata":{"id":"org.ekstep.lit.moon_cap.story","ver":"12"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"HIDE","extype":"","id":"flying_cap1","stageid":"scene3","uri":"","subtype":"","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:30+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.721Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:17:40.997+0530","ets":1462427260997,"ver":"2.0","gdata":{"id":"org.ekstep.lit.moon_cap.story","ver":"12"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"SHOW","extype":"","id":"flying_cap2","stageid":"scene3","uri":"","subtype":"","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:30+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.721Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:17:47.730+0530","ets":1462427267730,"ver":"2.0","gdata":{"id":"org.ekstep.lit.moon_cap.story","ver":"12"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"TOUCH","extype":"","id":"next","stageid":"scene3","uri":"","subtype":"","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:30+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.721Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:17:47.919+0530","ets":1462427267919,"ver":"2.0","gdata":{"id":"org.ekstep.lit.moon_cap.story","ver":"12"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"scene4:scene4_sound","stageid":"scene4","uri":"","subtype":"PLAY","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:30+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.721Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:17:48.111+0530","ets":1462427268111,"ver":"2.0","gdata":{"id":"org.ekstep.lit.moon_cap.story","ver":"12"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"scene4:leaves_sound","stageid":"scene4","uri":"","subtype":"PLAY","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:30+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.721Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:17:48.119+0530","ets":1462427268119,"ver":"2.0","gdata":{"id":"org.ekstep.lit.moon_cap.story","ver":"12"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"scene3:scene3_sound","stageid":"scene4","uri":"","subtype":"STOP","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:30+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.721Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:17:52.574+0530","ets":1462427272574,"ver":"2.0","gdata":{"id":"org.ekstep.lit.moon_cap.story","ver":"12"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"TOUCH","extype":"","id":"next","stageid":"scene4","uri":"","subtype":"","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:30+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.721Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:17:52.769+0530","ets":1462427272769,"ver":"2.0","gdata":{"id":"org.ekstep.lit.moon_cap.story","ver":"12"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"scene5:scene5_sound","stageid":"scene5","uri":"","subtype":"PLAY","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:30+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.721Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:17:52.845+0530","ets":1462427272845,"ver":"2.0","gdata":{"id":"org.ekstep.lit.moon_cap.story","ver":"12"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"scene5:crying_sound","stageid":"scene5","uri":"","subtype":"PLAY","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:30+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.722Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:17:52.854+0530","ets":1462427272854,"ver":"2.0","gdata":{"id":"org.ekstep.lit.moon_cap.story","ver":"12"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"scene4:scene4_sound","stageid":"scene5","uri":"","subtype":"STOP","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:30+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.722Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:17:52.861+0530","ets":1462427272861,"ver":"2.0","gdata":{"id":"org.ekstep.lit.moon_cap.story","ver":"12"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"scene4:leaves_sound","stageid":"scene5","uri":"","subtype":"STOP","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:30+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.722Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:18:07.127+0530","ets":1462427287127,"ver":"2.0","gdata":{"id":"org.ekstep.lit.moon_cap.story","ver":"12"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"TOUCH","extype":"","id":"next","stageid":"scene5","uri":"","subtype":"","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:30+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.722Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:18:07.327+0530","ets":1462427287327,"ver":"2.0","gdata":{"id":"org.ekstep.lit.moon_cap.story","ver":"12"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"scene6:scene6_sound","stageid":"scene6","uri":"","subtype":"PLAY","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:30+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.722Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:18:07.422+0530","ets":1462427287422,"ver":"2.0","gdata":{"id":"org.ekstep.lit.moon_cap.story","ver":"12"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"scene6:evening_sound","stageid":"scene6","uri":"","subtype":"PLAY","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:30+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.722Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:18:15.389+0530","ets":1462427295389,"ver":"2.0","gdata":{"id":"org.ekstep.lit.moon_cap.story","ver":"12"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"TOUCH","extype":"","id":"next","stageid":"scene6","uri":"","subtype":"","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:30+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.722Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:18:15.572+0530","ets":1462427295572,"ver":"2.0","gdata":{"id":"org.ekstep.lit.moon_cap.story","ver":"12"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"scene7:scene7_sound","stageid":"scene7","uri":"","subtype":"PLAY","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:30+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.723Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:18:22.414+0530","ets":1462427302414,"ver":"2.0","gdata":{"id":"org.ekstep.lit.moon_cap.story","ver":"12"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"TOUCH","extype":"","id":"next","stageid":"scene7","uri":"","subtype":"","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:30+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.723Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:18:22.644+0530","ets":1462427302644,"ver":"2.0","gdata":{"id":"org.ekstep.lit.moon_cap.story","ver":"12"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"scene8:scene8_sound","stageid":"scene8","uri":"","subtype":"PLAY","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:30+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.723Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:18:22.658+0530","ets":1462427302658,"ver":"2.0","gdata":{"id":"org.ekstep.lit.moon_cap.story","ver":"12"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"scene7:scene7_sound","stageid":"scene8","uri":"","subtype":"STOP","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:30+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.723Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:18:31.837+0530","ets":1462427311837,"ver":"2.0","gdata":{"id":"org.ekstep.lit.moon_cap.story","ver":"12"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"TOUCH","extype":"","id":"next","stageid":"scene8","uri":"","subtype":"","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:30+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.723Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:18:32.042+0530","ets":1462427312042,"ver":"2.0","gdata":{"id":"org.ekstep.lit.moon_cap.story","ver":"12"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"scene9:scene9_sound","stageid":"scene9","uri":"","subtype":"PLAY","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:30+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.723Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:18:32.296+0530","ets":1462427312297,"ver":"2.0","gdata":{"id":"org.ekstep.lit.moon_cap.story","ver":"12"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"scene9:outro_sound","stageid":"scene9","uri":"","subtype":"PLAY","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:30+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.723Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:18:39.746+0530","ets":1462427319746,"ver":"2.0","gdata":{"id":"org.ekstep.lit.moon_cap.story","ver":"12"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"TOUCH","extype":"","id":"next","stageid":"scene9","uri":"","subtype":"","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:30+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.724Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:18:39.909+0530","ets":1462427319909,"ver":"2.0","gdata":{"id":"org.ekstep.lit.moon_cap.story","ver":"12"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"scene10:scene10_sound","stageid":"scene10","uri":"","subtype":"PLAY","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:30+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.724Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:18:40.151+0530","ets":1462427320151,"ver":"2.0","gdata":{"id":"org.ekstep.lit.moon_cap.story","ver":"12"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"scene10:intro_sound","stageid":"scene10","uri":"","subtype":"PLAY","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:30+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.724Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:18:40.180+0530","ets":1462427320180,"ver":"2.0","gdata":{"id":"org.ekstep.lit.moon_cap.story","ver":"12"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"scene9:outro_sound","stageid":"scene10","uri":"","subtype":"STOP","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:30+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.724Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:18:43.584+0530","ets":1462427323584,"ver":"2.0","gdata":{"id":"org.ekstep.lit.moon_cap.story","ver":"12"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"TOUCH","extype":"","id":"next","stageid":"scene10","uri":"","subtype":"","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:30+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.724Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:18:43.810+0530","ets":1462427323810,"ver":"2.0","gdata":{"id":"org.ekstep.lit.moon_cap.story","ver":"12"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"scene11:ft_1_sound","stageid":"scene11","uri":"","subtype":"PLAY","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:30+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.724Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:18:43.817+0530","ets":1462427323817,"ver":"2.0","gdata":{"id":"org.ekstep.lit.moon_cap.story","ver":"12"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"scene10:intro_sound","stageid":"scene11","uri":"","subtype":"STOP","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:30+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.724Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:18:49.101+0530","ets":1462427329101,"ver":"2.0","gdata":{"id":"org.ekstep.lit.moon_cap.story","ver":"12"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"DROP","extype":"","id":"ws_sun","stageid":"scene11","uri":"","subtype":"","pos":[{"x":190.82899475097656,"y":250.24398803710938},{"x":616.7849731445312,"y":420.0}],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:30+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.725Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:18:50.489+0530","ets":1462427330489,"ver":"2.0","gdata":{"id":"org.ekstep.lit.moon_cap.story","ver":"12"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"DROP","extype":"","id":"ws_moon","stageid":"scene11","uri":"","subtype":"","pos":[{"x":405.510986328125,"y":223.90199279785156},{"x":431.0679931640625,"y":427.02398681640625}],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:30+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.725Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:18:54.182+0530","ets":1462427334182,"ver":"2.0","gdata":{"id":"org.ekstep.lit.moon_cap.story","ver":"12"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"DROP","extype":"","id":"ws_tree","stageid":"scene11","uri":"","subtype":"","pos":[{"x":979.7000122070312,"y":365.85400390625},{"x":151.64100646972656,"y":446.927001953125}],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:30+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.725Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:18:54.580+0530","ets":1462427334580,"ver":"2.0","gdata":{"id":"org.ekstep.lit.moon_cap.story","ver":"12"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"DROP","extype":"","id":"ws_bluecap","stageid":"scene11","uri":"","subtype":"","pos":[{"x":644.0469970703125,"y":420.0},{"x":793.9829711914062,"y":449.26800537109375}],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:30+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.725Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:18:57.630+0530","ets":1462427337630,"ver":"2.0","gdata":{"id":"org.ekstep.lit.moon_cap.story","ver":"12"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"DROP","extype":"","id":"ws_bluecap","stageid":"scene11","uri":"","subtype":"","pos":[{"x":584.4130249023438,"y":218.63400268554688},{"x":824.6519775390625,"y":416.4880065917969}],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:30+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.725Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:19:00.061+0530","ets":1462427340061,"ver":"2.0","gdata":{"id":"org.ekstep.lit.moon_cap.story","ver":"12"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"DROP","extype":"","id":"ws_tree","stageid":"scene11","uri":"","subtype":"","pos":[{"x":811.02197265625,"y":301.7560119628906},{"x":182.3090057373047,"y":456.2929992675781}],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:30+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.726Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:19:02.595+0530","ets":1462427342595,"ver":"2.0","gdata":{"id":"org.ekstep.lit.moon_cap.story","ver":"12"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"TOUCH","extype":"","id":"next","stageid":"scene11","uri":"","subtype":"","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:30+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.726Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:19:03.197+0530","ets":1462427343197,"ver":"2.0","gdata":{"id":"org.ekstep.lit.moon_cap.story","ver":"12"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"TOUCH","extype":"","id":"next","stageid":"scene12","uri":"","subtype":"","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:30+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.726Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:19:03.462+0530","ets":1462427343462,"ver":"2.0","gdata":{"id":"org.ekstep.lit.moon_cap.story","ver":"12"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"scene13:ft_4_sound","stageid":"scene13","uri":"","subtype":"PLAY","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:30+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.726Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:19:05.521+0530","ets":1462427345521,"ver":"2.0","gdata":{"id":"org.ekstep.lit.moon_cap.story","ver":"12"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"TOUCH","extype":"","id":"previous","stageid":"scene13","uri":"","subtype":"","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:30+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.726Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:19:14.098+0530","ets":1462427354098,"ver":"2.0","gdata":{"id":"org.ekstep.lit.moon_cap.story","ver":"12"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"TOUCH","extype":"","id":"next","stageid":"scene12","uri":"","subtype":"","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:30+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.726Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:19:14.262+0530","ets":1462427354262,"ver":"2.0","gdata":{"id":"org.ekstep.lit.moon_cap.story","ver":"12"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"scene13:ft_4_sound","stageid":"scene13","uri":"","subtype":"PLAY","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:30+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.726Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:19:17.003+0530","ets":1462427357003,"ver":"2.0","gdata":{"id":"org.ekstep.lit.moon_cap.story","ver":"12"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"TOUCH","extype":"","id":"previous","stageid":"scene13","uri":"","subtype":"","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:30+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.727Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:19:17.464+0530","ets":1462427357464,"ver":"2.0","gdata":{"id":"org.ekstep.lit.moon_cap.story","ver":"12"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"TOUCH","extype":"","id":"previous","stageid":"scene12","uri":"","subtype":"","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:30+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.727Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:19:17.653+0530","ets":1462427357653,"ver":"2.0","gdata":{"id":"org.ekstep.lit.moon_cap.story","ver":"12"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"scene11:ft_1_sound","stageid":"scene11","uri":"","subtype":"PLAY","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:30+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.727Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:19:19.547+0530","ets":1462427359547,"ver":"2.0","gdata":{"id":"org.ekstep.lit.moon_cap.story","ver":"12"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"TOUCH","extype":"","id":"next","stageid":"scene11","uri":"","subtype":"","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:30+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.727Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:19:19.721+0530","ets":1462427359721,"ver":"2.0","gdata":{"id":"org.ekstep.lit.moon_cap.story","ver":"12"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"scene11:ft_1_sound","stageid":"scene12","uri":"","subtype":"STOP","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:30+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.727Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:19:20.770+0530","ets":1462427360770,"ver":"2.0","gdata":{"id":"org.ekstep.lit.moon_cap.story","ver":"12"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"TOUCH","extype":"","id":"previous","stageid":"scene12","uri":"","subtype":"","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:30+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.728Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:19:20.921+0530","ets":1462427360921,"ver":"2.0","gdata":{"id":"org.ekstep.lit.moon_cap.story","ver":"12"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"scene11:ft_1_sound","stageid":"scene11","uri":"","subtype":"PLAY","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:30+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.728Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:19:23.758+0530","ets":1462427363758,"ver":"2.0","gdata":{"id":"org.ekstep.lit.moon_cap.story","ver":"12"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"DROP","extype":"","id":"ws_tree","stageid":"scene11","uri":"","subtype":"","pos":[{"x":811.02197265625,"y":267.80499267578125},{"x":194.23599243164062,"y":429.95098876953125}],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:30+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.728Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:19:25.825+0530","ets":1462427365825,"ver":"2.0","gdata":{"id":"org.ekstep.lit.moon_cap.story","ver":"12"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"DROP","extype":"","id":"ws_bluecap","stageid":"scene11","uri":"","subtype":"","pos":[{"x":579.0,"y":276.0},{"x":831.468017578125,"y":445.7560119628906}],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:30+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.728Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:19:27.689+0530","ets":1462427367689,"ver":"2.0","gdata":{"id":"org.ekstep.lit.moon_cap.story","ver":"12"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"DROP","extype":"","id":"ws_sun","stageid":"scene11","uri":"","subtype":"","pos":[{"x":243.64700317382812,"y":230.17100524902344},{"x":642.343017578125,"y":415.3169860839844}],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:30+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.728Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:19:29.319+0530","ets":1462427369319,"ver":"2.0","gdata":{"id":"org.ekstep.lit.moon_cap.story","ver":"12"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"DROP","extype":"","id":"ws_moon","stageid":"scene11","uri":"","subtype":"","pos":[{"x":414.0299987792969,"y":273.65899658203125},{"x":419.1409912109375,"y":460.3900146484375}],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:30+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.729Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:19:31.219+0530","ets":1462427371219,"ver":"2.0","gdata":{"id":"org.ekstep.lit.moon_cap.story","ver":"12"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"TOUCH","extype":"","id":"next","stageid":"scene11","uri":"","subtype":"","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:30+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.729Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:19:38.130+0530","ets":1462427378130,"ver":"2.0","gdata":{"id":"org.ekstep.lit.moon_cap.story","ver":"12"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"TOUCH","extype":"","id":"ft_3_map_sound","stageid":"scene12","uri":"","subtype":"","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:30+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.729Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:19:38.177+0530","ets":1462427378177,"ver":"2.0","gdata":{"id":"org.ekstep.lit.moon_cap.story","ver":"12"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"scene12:ft_3_map_sound","stageid":"scene12","uri":"","subtype":"PLAY","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:30+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.729Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:19:38.356+0530","ets":1462427378356,"ver":"2.0","gdata":{"id":"org.ekstep.lit.moon_cap.story","ver":"12"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"TOUCH","extype":"","id":"ft_3_map_sound","stageid":"scene12","uri":"","subtype":"","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:30+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.729Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:19:38.361+0530","ets":1462427378361,"ver":"2.0","gdata":{"id":"org.ekstep.lit.moon_cap.story","ver":"12"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"scene12:ft_3_map_sound","stageid":"scene12","uri":"","subtype":"PLAY","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:30+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.729Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:19:38.866+0530","ets":1462427378866,"ver":"2.0","gdata":{"id":"org.ekstep.lit.moon_cap.story","ver":"12"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"TOUCH","extype":"","id":"ft_3_map_sound","stageid":"scene12","uri":"","subtype":"","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:30+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.729Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:19:38.869+0530","ets":1462427378869,"ver":"2.0","gdata":{"id":"org.ekstep.lit.moon_cap.story","ver":"12"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"scene12:ft_3_map_sound","stageid":"scene12","uri":"","subtype":"PLAY","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:30+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.729Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:19:42.123+0530","ets":1462427382123,"ver":"2.0","gdata":{"id":"org.ekstep.lit.moon_cap.story","ver":"12"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"TOUCH","extype":"","id":"ft_3_noon_sound","stageid":"scene12","uri":"","subtype":"","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:30+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.729Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:19:42.176+0530","ets":1462427382176,"ver":"2.0","gdata":{"id":"org.ekstep.lit.moon_cap.story","ver":"12"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"scene12:ft_3_noon_sound","stageid":"scene12","uri":"","subtype":"PLAY","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:30+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.730Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:19:42.349+0530","ets":1462427382349,"ver":"2.0","gdata":{"id":"org.ekstep.lit.moon_cap.story","ver":"12"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"TOUCH","extype":"","id":"ft_3_goon_sound","stageid":"scene12","uri":"","subtype":"","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:30+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.730Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:19:42.390+0530","ets":1462427382390,"ver":"2.0","gdata":{"id":"org.ekstep.lit.moon_cap.story","ver":"12"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"scene12:ft_3_goon_sound","stageid":"scene12","uri":"","subtype":"PLAY","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:30+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.730Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:19:44.566+0530","ets":1462427384566,"ver":"2.0","gdata":{"id":"org.ekstep.lit.moon_cap.story","ver":"12"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"TOUCH","extype":"","id":"next","stageid":"scene12","uri":"","subtype":"","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:30+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.730Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:19:44.832+0530","ets":1462427384832,"ver":"2.0","gdata":{"id":"org.ekstep.lit.moon_cap.story","ver":"12"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"scene13:ft_4_sound","stageid":"scene13","uri":"","subtype":"PLAY","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:30+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.730Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:19:50.757+0530","ets":1462427390757,"ver":"2.0","gdata":{"id":"org.ekstep.lit.moon_cap.story","ver":"12"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"TOUCH","extype":"","id":"next","stageid":"scene13","uri":"","subtype":"","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:30+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.730Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:19:50.925+0530","ets":1462427390925,"ver":"2.0","gdata":{"id":"org.ekstep.lit.moon_cap.story","ver":"12"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"scene14_1:ws_3_sound","stageid":"scene14_1","uri":"","subtype":"PLAY","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:30+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.730Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:19:51.161+0530","ets":1462427391161,"ver":"2.0","gdata":{"id":"org.ekstep.lit.moon_cap.story","ver":"12"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"TOUCH","extype":"","id":"next","stageid":"scene14_1","uri":"","subtype":"","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:30+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.730Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:19:51.302+0530","ets":1462427391302,"ver":"2.0","gdata":{"id":"org.ekstep.lit.moon_cap.story","ver":"12"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"scene14_2:ws_3_sound1","stageid":"scene14_2","uri":"","subtype":"PLAY","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:30+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.731Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:19:51.312+0530","ets":1462427391312,"ver":"2.0","gdata":{"id":"org.ekstep.lit.moon_cap.story","ver":"12"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"scene14_1:ws_3_sound","stageid":"scene14_2","uri":"","subtype":"STOP","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:30+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.731Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:19:54.356+0530","ets":1462427394356,"ver":"2.0","gdata":{"id":"org.ekstep.lit.moon_cap.story","ver":"12"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"scene14_2:ws_3_redcap_sound","stageid":"scene14_2","uri":"","subtype":"PLAY","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:30+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.731Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:19:54.675+0530","ets":1462427394675,"ver":"2.0","gdata":{"id":"org.ekstep.lit.moon_cap.story","ver":"12"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"TOUCH","extype":"","id":"ws_3_redcap_sound","stageid":"scene14_2","uri":"","subtype":"","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:30+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.731Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:19:54.679+0530","ets":1462427394679,"ver":"2.0","gdata":{"id":"org.ekstep.lit.moon_cap.story","ver":"12"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"scene14_2:ws_3_redcap_sound","stageid":"scene14_2","uri":"","subtype":"PLAY","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:30+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.731Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:19:54.685+0530","ets":1462427394685,"ver":"2.0","gdata":{"id":"org.ekstep.lit.moon_cap.story","ver":"12"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"SHOW","extype":"","id":"glow_redcap","stageid":"scene14_2","uri":"","subtype":"","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:30+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.731Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:19:57.493+0530","ets":1462427397493,"ver":"2.0","gdata":{"id":"org.ekstep.lit.moon_cap.story","ver":"12"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"TOUCH","extype":"","id":"ws_3_redcap_sound","stageid":"scene14_2","uri":"","subtype":"","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:30+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.731Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:19:57.516+0530","ets":1462427397516,"ver":"2.0","gdata":{"id":"org.ekstep.lit.moon_cap.story","ver":"12"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"scene14_2:ws_3_redcap_sound","stageid":"scene14_2","uri":"","subtype":"PLAY","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:30+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.731Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:19:58.591+0530","ets":1462427398591,"ver":"2.0","gdata":{"id":"org.ekstep.lit.moon_cap.story","ver":"12"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"TOUCH","extype":"","id":"ws_3_redcap_sound","stageid":"scene14_2","uri":"","subtype":"","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:30+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.732Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:19:58.597+0530","ets":1462427398597,"ver":"2.0","gdata":{"id":"org.ekstep.lit.moon_cap.story","ver":"12"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"scene14_2:ws_3_redcap_sound","stageid":"scene14_2","uri":"","subtype":"PLAY","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:30+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.732Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:19:58.747+0530","ets":1462427398747,"ver":"2.0","gdata":{"id":"org.ekstep.lit.moon_cap.story","ver":"12"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"TOUCH","extype":"","id":"ws_3_redcap_sound","stageid":"scene14_2","uri":"","subtype":"","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:30+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.732Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:19:58.750+0530","ets":1462427398750,"ver":"2.0","gdata":{"id":"org.ekstep.lit.moon_cap.story","ver":"12"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"scene14_2:ws_3_redcap_sound","stageid":"scene14_2","uri":"","subtype":"PLAY","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:30+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.732Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:19:59.208+0530","ets":1462427399208,"ver":"2.0","gdata":{"id":"org.ekstep.lit.moon_cap.story","ver":"12"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"TOUCH","extype":"","id":"ws_3_redcap_sound","stageid":"scene14_2","uri":"","subtype":"","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:30+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.732Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:19:59.236+0530","ets":1462427399236,"ver":"2.0","gdata":{"id":"org.ekstep.lit.moon_cap.story","ver":"12"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"scene14_2:ws_3_redcap_sound","stageid":"scene14_2","uri":"","subtype":"PLAY","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:30+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.732Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:19:59.828+0530","ets":1462427399828,"ver":"2.0","gdata":{"id":"org.ekstep.lit.moon_cap.story","ver":"12"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"TOUCH","extype":"","id":"ws_3_redcap_sound","stageid":"scene14_2","uri":"","subtype":"","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:30+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.732Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:19:59.831+0530","ets":1462427399831,"ver":"2.0","gdata":{"id":"org.ekstep.lit.moon_cap.story","ver":"12"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"scene14_2:ws_3_redcap_sound","stageid":"scene14_2","uri":"","subtype":"PLAY","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:30+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.732Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:20:00.097+0530","ets":1462427400097,"ver":"2.0","gdata":{"id":"org.ekstep.lit.moon_cap.story","ver":"12"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"TOUCH","extype":"","id":"ws_3_redcap_sound","stageid":"scene14_2","uri":"","subtype":"","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:30+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.732Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:20:00.099+0530","ets":1462427400099,"ver":"2.0","gdata":{"id":"org.ekstep.lit.moon_cap.story","ver":"12"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"scene14_2:ws_3_redcap_sound","stageid":"scene14_2","uri":"","subtype":"PLAY","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:30+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.732Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:20:00.324+0530","ets":1462427400324,"ver":"2.0","gdata":{"id":"org.ekstep.lit.moon_cap.story","ver":"12"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"TOUCH","extype":"","id":"ws_3_redcap_sound","stageid":"scene14_2","uri":"","subtype":"","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:30+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.733Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:20:00.328+0530","ets":1462427400328,"ver":"2.0","gdata":{"id":"org.ekstep.lit.moon_cap.story","ver":"12"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"scene14_2:ws_3_redcap_sound","stageid":"scene14_2","uri":"","subtype":"PLAY","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:30+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.733Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:20:00.537+0530","ets":1462427400537,"ver":"2.0","gdata":{"id":"org.ekstep.lit.moon_cap.story","ver":"12"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"TOUCH","extype":"","id":"ws_3_redcap_sound","stageid":"scene14_2","uri":"","subtype":"","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:30+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.733Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:20:00.551+0530","ets":1462427400551,"ver":"2.0","gdata":{"id":"org.ekstep.lit.moon_cap.story","ver":"12"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"scene14_2:ws_3_redcap_sound","stageid":"scene14_2","uri":"","subtype":"PLAY","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:30+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.733Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:20:00.790+0530","ets":1462427400790,"ver":"2.0","gdata":{"id":"org.ekstep.lit.moon_cap.story","ver":"12"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"TOUCH","extype":"","id":"ws_3_redcap_sound","stageid":"scene14_2","uri":"","subtype":"","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:30+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.733Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:20:00.796+0530","ets":1462427400796,"ver":"2.0","gdata":{"id":"org.ekstep.lit.moon_cap.story","ver":"12"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"scene14_2:ws_3_redcap_sound","stageid":"scene14_2","uri":"","subtype":"PLAY","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:30+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.733Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:20:01.006+0530","ets":1462427401007,"ver":"2.0","gdata":{"id":"org.ekstep.lit.moon_cap.story","ver":"12"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"TOUCH","extype":"","id":"ws_3_redcap_sound","stageid":"scene14_2","uri":"","subtype":"","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:30+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.733Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:20:01.012+0530","ets":1462427401012,"ver":"2.0","gdata":{"id":"org.ekstep.lit.moon_cap.story","ver":"12"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"scene14_2:ws_3_redcap_sound","stageid":"scene14_2","uri":"","subtype":"PLAY","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:30+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.733Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:20:01.215+0530","ets":1462427401215,"ver":"2.0","gdata":{"id":"org.ekstep.lit.moon_cap.story","ver":"12"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"TOUCH","extype":"","id":"ws_3_redcap_sound","stageid":"scene14_2","uri":"","subtype":"","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:30+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.733Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:20:01.218+0530","ets":1462427401218,"ver":"2.0","gdata":{"id":"org.ekstep.lit.moon_cap.story","ver":"12"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"scene14_2:ws_3_redcap_sound","stageid":"scene14_2","uri":"","subtype":"PLAY","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:30+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.734Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:20:01.877+0530","ets":1462427401877,"ver":"2.0","gdata":{"id":"org.ekstep.lit.moon_cap.story","ver":"12"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"TOUCH","extype":"","id":"ws_3_redcap_sound","stageid":"scene14_2","uri":"","subtype":"","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:30+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.734Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:20:01.880+0530","ets":1462427401880,"ver":"2.0","gdata":{"id":"org.ekstep.lit.moon_cap.story","ver":"12"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"scene14_2:ws_3_redcap_sound","stageid":"scene14_2","uri":"","subtype":"PLAY","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:30+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.734Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:20:02.085+0530","ets":1462427402085,"ver":"2.0","gdata":{"id":"org.ekstep.lit.moon_cap.story","ver":"12"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"TOUCH","extype":"","id":"ws_3_redcap_sound","stageid":"scene14_2","uri":"","subtype":"","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:30+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.734Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:20:02.100+0530","ets":1462427402100,"ver":"2.0","gdata":{"id":"org.ekstep.lit.moon_cap.story","ver":"12"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"scene14_2:ws_3_redcap_sound","stageid":"scene14_2","uri":"","subtype":"PLAY","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:30+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.734Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:20:02.266+0530","ets":1462427402266,"ver":"2.0","gdata":{"id":"org.ekstep.lit.moon_cap.story","ver":"12"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"TOUCH","extype":"","id":"ws_3_redcap_sound","stageid":"scene14_2","uri":"","subtype":"","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:30+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.734Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:20:02.268+0530","ets":1462427402268,"ver":"2.0","gdata":{"id":"org.ekstep.lit.moon_cap.story","ver":"12"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"scene14_2:ws_3_redcap_sound","stageid":"scene14_2","uri":"","subtype":"PLAY","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:30+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.734Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:20:04.141+0530","ets":1462427404141,"ver":"2.0","gdata":{"id":"org.ekstep.lit.moon_cap.story","ver":"12"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"TOUCH","extype":"","id":"next","stageid":"scene14_2","uri":"","subtype":"","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:30+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.734Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:20:04.322+0530","ets":1462427404322,"ver":"2.0","gdata":{"id":"org.ekstep.lit.moon_cap.story","ver":"12"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"scene14_3:ws_3_sound2","stageid":"scene14_3","uri":"","subtype":"PLAY","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:30+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.734Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:20:05.543+0530","ets":1462427405543,"ver":"2.0","gdata":{"id":"org.ekstep.lit.moon_cap.story","ver":"12"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"TOUCH","extype":"","id":"next","stageid":"scene14_3","uri":"","subtype":"","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:30+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.735Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:20:05.873+0530","ets":1462427405873,"ver":"2.0","gdata":{"id":"org.ekstep.lit.moon_cap.story","ver":"12"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"scene15:ws_8_sound","stageid":"scene15","uri":"","subtype":"PLAY","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:30+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.735Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:20:14.593+0530","ets":1462427414593,"ver":"2.0","gdata":{"id":"org.ekstep.lit.moon_cap.story","ver":"12"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"CHOOSE","extype":"","id":"Strong","stageid":"scene15","uri":"","subtype":"","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:30+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.735Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:20:14.822+0530","ets":1462427414822,"ver":"2.0","gdata":{"id":"org.ekstep.lit.moon_cap.story","ver":"12"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"CHOOSE","extype":"","id":"Strong","stageid":"scene15","uri":"","subtype":"","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:30+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.735Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:20:16.296+0530","ets":1462427416296,"ver":"2.0","gdata":{"id":"org.ekstep.lit.moon_cap.story","ver":"12"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"CHOOSE","extype":"","id":"Strong","stageid":"scene15","uri":"","subtype":"","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:30+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.735Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:20:16.404+0530","ets":1462427416404,"ver":"2.0","gdata":{"id":"org.ekstep.lit.moon_cap.story","ver":"12"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"CHOOSE","extype":"","id":"Strong","stageid":"scene15","uri":"","subtype":"","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:30+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.736Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:20:17.835+0530","ets":1462427417835,"ver":"2.0","gdata":{"id":"org.ekstep.lit.moon_cap.story","ver":"12"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"CHOOSE","extype":"","id":"Strong","stageid":"scene15","uri":"","subtype":"","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:30+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.736Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:20:19.979+0530","ets":1462427419979,"ver":"2.0","gdata":{"id":"org.ekstep.lit.moon_cap.story","ver":"12"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"CHOOSE","extype":"","id":"Strong","stageid":"scene15","uri":"","subtype":"","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:30+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.736Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:20:21.723+0530","ets":1462427421723,"ver":"2.0","gdata":{"id":"org.ekstep.lit.moon_cap.story","ver":"12"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"CHOOSE","extype":"","id":"Strong","stageid":"scene15","uri":"","subtype":"","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:30+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.736Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:20:27.521+0530","ets":1462427427521,"ver":"2.0","gdata":{"id":"org.ekstep.lit.moon_cap.story","ver":"12"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"TOUCH","extype":"","id":"next","stageid":"scene15","uri":"","subtype":"","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:30+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.736Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:20:27.713+0530","ets":1462427427713,"ver":"2.0","gdata":{"id":"org.ekstep.lit.moon_cap.story","ver":"12"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"scene15:ws_2_sound","stageid":"scene15","uri":"","subtype":"PLAY","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:30+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.736Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:20:31.164+0530","ets":1462427431164,"ver":"2.0","gdata":{"id":"org.ekstep.lit.moon_cap.story","ver":"12"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"CHOOSE","extype":"","id":"A","stageid":"scene15","uri":"","subtype":"","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:30+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.737Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:20:37.270+0530","ets":1462427437270,"ver":"2.0","gdata":{"id":"org.ekstep.lit.moon_cap.story","ver":"12"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"TOUCH","extype":"","id":"next","stageid":"scene15","uri":"","subtype":"","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:30+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.737Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:20:37.427+0530","ets":1462427437427,"ver":"2.0","gdata":{"id":"org.ekstep.lit.moon_cap.story","ver":"12"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"scene15:ws_2_sound","stageid":"scene15","uri":"","subtype":"PLAY","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:30+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.737Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:20:40.303+0530","ets":1462427440303,"ver":"2.0","gdata":{"id":"org.ekstep.lit.moon_cap.story","ver":"12"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"CHOOSE","extype":"","id":"U","stageid":"scene15","uri":"","subtype":"","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:30+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.737Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:20:51.643+0530","ets":1462427451643,"ver":"2.0","gdata":{"id":"org.ekstep.lit.moon_cap.story","ver":"12"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"CHOOSE","extype":"","id":"U","stageid":"scene15","uri":"","subtype":"","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:31+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.738Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:20:53.143+0530","ets":1462427453143,"ver":"2.0","gdata":{"id":"org.ekstep.lit.moon_cap.story","ver":"12"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"CHOOSE","extype":"","id":"U","stageid":"scene15","uri":"","subtype":"","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:31+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.738Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:20:56.651+0530","ets":1462427456651,"ver":"2.0","gdata":{"id":"org.ekstep.lit.moon_cap.story","ver":"12"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"TOUCH","extype":"","id":"next","stageid":"scene15","uri":"","subtype":"","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:31+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.738Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:20:56.937+0530","ets":1462427456937,"ver":"2.0","gdata":{"id":"org.ekstep.lit.moon_cap.story","ver":"12"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"scene15:ws_5_sound","stageid":"scene15","uri":"","subtype":"PLAY","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:31+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.738Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:21:01.997+0530","ets":1462427461997,"ver":"2.0","gdata":{"id":"org.ekstep.lit.moon_cap.story","ver":"12"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"CHOOSE","extype":"","id":"Fair","stageid":"scene15","uri":"","subtype":"","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:31+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.738Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:21:06.083+0530","ets":1462427466083,"ver":"2.0","gdata":{"id":"org.ekstep.lit.moon_cap.story","ver":"12"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"TOUCH","extype":"","id":"next","stageid":"scene15","uri":"","subtype":"","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:31+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.738Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:21:06.313+0530","ets":1462427466313,"ver":"2.0","gdata":{"id":"org.ekstep.lit.moon_cap.story","ver":"12"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"scene15:ws_4_sound","stageid":"scene15","uri":"","subtype":"PLAY","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:31+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.739Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:21:09.920+0530","ets":1462427469920,"ver":"2.0","gdata":{"id":"org.ekstep.lit.moon_cap.story","ver":"12"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"CHOOSE","extype":"","id":"ws_red","stageid":"scene15","uri":"","subtype":"","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:31+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.739Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:21:15.332+0530","ets":1462427475332,"ver":"2.0","gdata":{"id":"org.ekstep.lit.moon_cap.story","ver":"12"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"TOUCH","extype":"","id":"next","stageid":"scene15","uri":"","subtype":"","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:31+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.739Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:21:15.585+0530","ets":1462427475585,"ver":"2.0","gdata":{"id":"org.ekstep.lit.moon_cap.story","ver":"12"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"scene15:ws_7_sound","stageid":"scene15","uri":"","subtype":"PLAY","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:31+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.739Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:21:18.498+0530","ets":1462427478498,"ver":"2.0","gdata":{"id":"org.ekstep.lit.moon_cap.story","ver":"12"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"TOUCH","extype":"","id":"previous","stageid":"scene15","uri":"","subtype":"","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:31+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.739Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:21:18.818+0530","ets":1462427478818,"ver":"2.0","gdata":{"id":"org.ekstep.lit.moon_cap.story","ver":"12"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"scene15:ws_4_sound","stageid":"scene15","uri":"","subtype":"PLAY","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:31+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.739Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:21:20.393+0530","ets":1462427480393,"ver":"2.0","gdata":{"id":"org.ekstep.lit.moon_cap.story","ver":"12"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"CHOOSE","extype":"","id":"ws_blue","stageid":"scene15","uri":"","subtype":"","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:31+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.739Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:21:22.987+0530","ets":1462427482987,"ver":"2.0","gdata":{"id":"org.ekstep.lit.moon_cap.story","ver":"12"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"TOUCH","extype":"","id":"next","stageid":"scene15","uri":"","subtype":"","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:31+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.740Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:21:23.066+0530","ets":1462427483066,"ver":"2.0","gdata":{"id":"org.ekstep.lit.moon_cap.story","ver":"12"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"scene15:ws_7_sound","stageid":"scene15","uri":"","subtype":"STOP","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:31+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.740Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:21:23.214+0530","ets":1462427483214,"ver":"2.0","gdata":{"id":"org.ekstep.lit.moon_cap.story","ver":"12"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"scene15:ws_7_sound","stageid":"scene15","uri":"","subtype":"PLAY","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:31+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.740Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:21:30.303+0530","ets":1462427490303,"ver":"2.0","gdata":{"id":"org.ekstep.lit.moon_cap.story","ver":"12"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"CHOOSE","extype":"","id":"Bright","stageid":"scene15","uri":"","subtype":"","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:31+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.740Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:21:35.337+0530","ets":1462427495337,"ver":"2.0","gdata":{"id":"org.ekstep.lit.moon_cap.story","ver":"12"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"CHOOSE","extype":"","id":"Candy","stageid":"scene15","uri":"","subtype":"","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:31+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.740Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:21:37.601+0530","ets":1462427497601,"ver":"2.0","gdata":{"id":"org.ekstep.lit.moon_cap.story","ver":"12"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"CHOOSE","extype":"","id":"Bright","stageid":"scene15","uri":"","subtype":"","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:31+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.741Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:21:45.019+0530","ets":1462427505019,"ver":"2.0","gdata":{"id":"org.ekstep.lit.moon_cap.story","ver":"12"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"CHOOSE","extype":"","id":"Candy","stageid":"scene15","uri":"","subtype":"","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:31+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.741Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:21:52.389+0530","ets":1462427512389,"ver":"2.0","gdata":{"id":"org.ekstep.lit.moon_cap.story","ver":"12"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"CHOOSE","extype":"","id":"Strong","stageid":"scene15","uri":"","subtype":"","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:31+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.741Z"} diff --git a/src/test/resources/1234/part-00001 b/src/test/resources/1234/part-00001 deleted file mode 100644 index 03f45af..0000000 --- a/src/test/resources/1234/part-00001 +++ /dev/null @@ -1,277 +0,0 @@ -{"eid":"OE_INTERACT","ts":"2016-05-05T11:21:58.867+0530","ets":1462427518867,"ver":"2.0","gdata":{"id":"org.ekstep.lit.moon_cap.story","ver":"12"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"CHOOSE","extype":"","id":"Old","stageid":"scene15","uri":"","subtype":"","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:31+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.741Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:22:02.598+0530","ets":1462427522598,"ver":"2.0","gdata":{"id":"org.ekstep.lit.moon_cap.story","ver":"12"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"TOUCH","extype":"","id":"next","stageid":"scene15","uri":"","subtype":"","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:31+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.742Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:22:02.828+0530","ets":1462427522828,"ver":"2.0","gdata":{"id":"org.ekstep.lit.moon_cap.story","ver":"12"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"scene15:ws_2_sound","stageid":"scene15","uri":"","subtype":"PLAY","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:31+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.742Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:22:06.039+0530","ets":1462427526039,"ver":"2.0","gdata":{"id":"org.ekstep.lit.moon_cap.story","ver":"12"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"CHOOSE","extype":"","id":"E","stageid":"scene15","uri":"","subtype":"","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:31+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.742Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:22:06.627+0530","ets":1462427526627,"ver":"2.0","gdata":{"id":"org.ekstep.lit.moon_cap.story","ver":"12"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"CHOOSE","extype":"","id":"E","stageid":"scene15","uri":"","subtype":"","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:31+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.742Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:22:07.306+0530","ets":1462427527306,"ver":"2.0","gdata":{"id":"org.ekstep.lit.moon_cap.story","ver":"12"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"CHOOSE","extype":"","id":"E","stageid":"scene15","uri":"","subtype":"","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:31+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.742Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:22:10.870+0530","ets":1462427530870,"ver":"2.0","gdata":{"id":"org.ekstep.lit.moon_cap.story","ver":"12"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"TOUCH","extype":"","id":"next","stageid":"scene15","uri":"","subtype":"","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:31+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.743Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:22:11.239+0530","ets":1462427531239,"ver":"2.0","gdata":{"id":"org.ekstep.lit.moon_cap.story","ver":"12"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"scene15:ws_1_sound","stageid":"scene15","uri":"","subtype":"PLAY","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:31+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.743Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:22:14.125+0530","ets":1462427534125,"ver":"2.0","gdata":{"id":"org.ekstep.lit.moon_cap.story","ver":"12"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"TOUCH","extype":"","id":"previous","stageid":"scene15","uri":"","subtype":"","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:31+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.743Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:22:14.347+0530","ets":1462427534347,"ver":"2.0","gdata":{"id":"org.ekstep.lit.moon_cap.story","ver":"12"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"scene15:ws_2_sound","stageid":"scene15","uri":"","subtype":"PLAY","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:31+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.743Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:22:22.999+0530","ets":1462427542999,"ver":"2.0","gdata":{"id":"org.ekstep.lit.moon_cap.story","ver":"12"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"CHOOSE","extype":"","id":"E","stageid":"scene15","uri":"","subtype":"","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:31+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.743Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:22:23.957+0530","ets":1462427543957,"ver":"2.0","gdata":{"id":"org.ekstep.lit.moon_cap.story","ver":"12"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"CHOOSE","extype":"","id":"E","stageid":"scene15","uri":"","subtype":"","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:31+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.743Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:22:26.792+0530","ets":1462427546792,"ver":"2.0","gdata":{"id":"org.ekstep.lit.moon_cap.story","ver":"12"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"TOUCH","extype":"","id":"next","stageid":"scene15","uri":"","subtype":"","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:31+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.744Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:22:27.000+0530","ets":1462427547000,"ver":"2.0","gdata":{"id":"org.ekstep.lit.moon_cap.story","ver":"12"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"scene15:ws_1_sound","stageid":"scene15","uri":"","subtype":"PLAY","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:31+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.744Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:22:30.336+0530","ets":1462427550336,"ver":"2.0","gdata":{"id":"org.ekstep.lit.moon_cap.story","ver":"12"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"DROP","extype":"","id":"ws_candy","stageid":"scene15","uri":"","subtype":"","pos":[{"x":509.4440002441406,"y":229.17100524902344},{"x":819.541015625,"y":407.12200927734375}],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:31+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.744Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:22:33.246+0530","ets":1462427553246,"ver":"2.0","gdata":{"id":"org.ekstep.lit.moon_cap.story","ver":"12"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"DROP","extype":"","id":"ws_fancyglasses","stageid":"scene15","uri":"","subtype":"","pos":[{"x":853.6170043945312,"y":274.8290100097656},{"x":224.90499877929688,"y":452.7799987792969}],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:31+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.744Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:22:34.525+0530","ets":1462427554525,"ver":"2.0","gdata":{"id":"org.ekstep.lit.moon_cap.story","ver":"12"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"DROP","extype":"","id":"ws_bluecap","stageid":"scene15","uri":"","subtype":"","pos":[{"x":193.25999450683594,"y":302.0060119628906},{"x":511.14801025390625,"y":459.80499267578125}],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:31+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.744Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:22:38.804+0530","ets":1462427558804,"ver":"2.0","gdata":{"id":"org.ekstep.lit.moon_cap.story","ver":"12"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"DROP","extype":"","id":"ws_fancyglasses","stageid":"scene15","uri":"","subtype":"","pos":[{"x":270.9079895019531,"y":420.5849914550781},{"x":845.0980224609375,"y":211.61000061035156}],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:31+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.745Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:22:38.867+0530","ets":1462427558867,"ver":"2.0","gdata":{"id":"org.ekstep.lit.moon_cap.story","ver":"12"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"DROP","extype":"","id":"ws_candy","stageid":"scene15","uri":"","subtype":"","pos":[{"x":479.4620056152344,"y":235.61000061035156},{"x":538.4089965820312,"y":235.61000061035156}],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:31+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.745Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:22:39.682+0530","ets":1462427559682,"ver":"2.0","gdata":{"id":"org.ekstep.lit.moon_cap.story","ver":"12"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"DROP","extype":"","id":"ws_bluecap","stageid":"scene15","uri":"","subtype":"","pos":[{"x":282.8349914550781,"y":211.61000061035156},{"x":250.46299743652344,"y":216.8780059814453}],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:31+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.745Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:22:41.551+0530","ets":1462427561551,"ver":"2.0","gdata":{"id":"org.ekstep.lit.moon_cap.story","ver":"12"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"DROP","extype":"","id":"ws_bluecap","stageid":"scene15","uri":"","subtype":"","pos":[{"x":836.5789794921875,"y":242.0489959716797},{"x":250.46299743652344,"y":216.8780059814453}],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:31+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.745Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:22:42.425+0530","ets":1462427562425,"ver":"2.0","gdata":{"id":"org.ekstep.lit.moon_cap.story","ver":"12"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"DROP","extype":"","id":"ws_fancyglasses","stageid":"scene15","uri":"","subtype":"","pos":[{"x":221.4969940185547,"y":317.5610046386719},{"x":231.72000122070312,"y":374.34100341796875}],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:31+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.745Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:22:42.945+0530","ets":1462427562945,"ver":"2.0","gdata":{"id":"org.ekstep.lit.moon_cap.story","ver":"12"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"DROP","extype":"","id":"ws_fancyglasses","stageid":"scene15","uri":"","subtype":"","pos":[{"x":157.02699279785156,"y":226.09300231933594},{"x":231.72000122070312,"y":374.34100341796875}],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:31+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.746Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:22:43.302+0530","ets":1462427563302,"ver":"2.0","gdata":{"id":"org.ekstep.lit.moon_cap.story","ver":"12"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"DROP","extype":"","id":"ws_fancyglasses","stageid":"scene15","uri":"","subtype":"","pos":[{"x":188.2729949951172,"y":202.53700256347656},{"x":185.7169952392578,"y":203.4149932861328}],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:31+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.746Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:22:45.046+0530","ets":1462427565046,"ver":"2.0","gdata":{"id":"org.ekstep.lit.moon_cap.story","ver":"12"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"DROP","extype":"","id":"ws_bluecap","stageid":"scene15","uri":"","subtype":"","pos":[{"x":812.7249755859375,"y":259.6099853515625},{"x":511.14801025390625,"y":455.7070007324219}],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:31+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.746Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:22:47.473+0530","ets":1462427567473,"ver":"2.0","gdata":{"id":"org.ekstep.lit.moon_cap.story","ver":"12"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"TOUCH","extype":"","id":"next","stageid":"scene15","uri":"","subtype":"","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:31+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.746Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:22:47.717+0530","ets":1462427567717,"ver":"2.0","gdata":{"id":"org.ekstep.lit.moon_cap.story","ver":"12"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"scene15:ws_6_sound","stageid":"scene15","uri":"","subtype":"PLAY","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:31+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.746Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:22:52.393+0530","ets":1462427572393,"ver":"2.0","gdata":{"id":"org.ekstep.lit.moon_cap.story","ver":"12"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"CHOOSE","extype":"","id":"Pipal tree","stageid":"scene15","uri":"","subtype":"","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:31+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.746Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:22:59.106+0530","ets":1462427579106,"ver":"2.0","gdata":{"id":"org.ekstep.lit.moon_cap.story","ver":"12"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"CHOOSE","extype":"","id":"Moon","stageid":"scene15","uri":"","subtype":"","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:31+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.747Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:22:59.211+0530","ets":1462427579211,"ver":"2.0","gdata":{"id":"org.ekstep.lit.moon_cap.story","ver":"12"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"CHOOSE","extype":"","id":"Moon","stageid":"scene15","uri":"","subtype":"","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:31+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.747Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:23:00.285+0530","ets":1462427580285,"ver":"2.0","gdata":{"id":"org.ekstep.lit.moon_cap.story","ver":"12"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"CHOOSE","extype":"","id":"Banana tree","stageid":"scene15","uri":"","subtype":"","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:31+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.747Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:23:05.400+0530","ets":1462427585400,"ver":"2.0","gdata":{"id":"org.ekstep.lit.moon_cap.story","ver":"12"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"TOUCH","extype":"","id":"next","stageid":"scene15","uri":"","subtype":"","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:31+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.747Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:23:05.622+0530","ets":1462427585622,"ver":"2.0","gdata":{"id":"org.ekstep.lit.moon_cap.story","ver":"12"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"scene15:ws_2_sound","stageid":"scene15","uri":"","subtype":"PLAY","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:31+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.748Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:23:07.177+0530","ets":1462427587177,"ver":"2.0","gdata":{"id":"org.ekstep.lit.moon_cap.story","ver":"12"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"TOUCH","extype":"","id":"previous","stageid":"scene15","uri":"","subtype":"","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:31+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.748Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:23:07.322+0530","ets":1462427587322,"ver":"2.0","gdata":{"id":"org.ekstep.lit.moon_cap.story","ver":"12"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"scene15:ws_6_sound","stageid":"scene15","uri":"","subtype":"PLAY","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:31+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.748Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:23:10.297+0530","ets":1462427590297,"ver":"2.0","gdata":{"id":"org.ekstep.lit.moon_cap.story","ver":"12"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"CHOOSE","extype":"","id":"Banana tree","stageid":"scene15","uri":"","subtype":"","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:31+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.748Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:23:13.477+0530","ets":1462427593477,"ver":"2.0","gdata":{"id":"org.ekstep.lit.moon_cap.story","ver":"12"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"TOUCH","extype":"","id":"next","stageid":"scene15","uri":"","subtype":"","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:31+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.748Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:23:13.564+0530","ets":1462427593564,"ver":"2.0","gdata":{"id":"org.ekstep.lit.moon_cap.story","ver":"12"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"scene15:ws_2_sound","stageid":"scene15","uri":"","subtype":"STOP","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:31+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.748Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:23:13.717+0530","ets":1462427593717,"ver":"2.0","gdata":{"id":"org.ekstep.lit.moon_cap.story","ver":"12"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"scene15:ws_2_sound","stageid":"scene15","uri":"","subtype":"PLAY","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:31+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.748Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:23:16.424+0530","ets":1462427596424,"ver":"2.0","gdata":{"id":"org.ekstep.lit.moon_cap.story","ver":"12"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"CHOOSE","extype":"","id":"O","stageid":"scene15","uri":"","subtype":"","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:31+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.749Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:23:17.128+0530","ets":1462427597128,"ver":"2.0","gdata":{"id":"org.ekstep.lit.moon_cap.story","ver":"12"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"CHOOSE","extype":"","id":"O","stageid":"scene15","uri":"","subtype":"","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:31+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.749Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:23:23.889+0530","ets":1462427603889,"ver":"2.0","gdata":{"id":"org.ekstep.lit.moon_cap.story","ver":"12"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"TOUCH","extype":"","id":"next","stageid":"scene15","uri":"","subtype":"","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:31+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.749Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:23:30.066+0530","ets":1462427610066,"ver":"2.0","gdata":{"id":"org.ekstep.lit.moon_cap.story","ver":"12"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"TOUCH","extype":"","id":"theme","stageid":"endScreen","uri":"","subtype":"","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:31+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.749Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:23:49.425+0530","ets":1462427629425,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.colours","ver":"1"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"splash:scene1_audio","stageid":"splash","uri":"","subtype":"PLAY","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:31+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.750Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:23:57.544+0530","ets":1462427637544,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.colours","ver":"1"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"TOUCH","extype":"","id":"next","stageid":"splash","uri":"","subtype":"","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:31+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.750Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:23:57.872+0530","ets":1462427637872,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.colours","ver":"1"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"scene2:scene2_audio","stageid":"scene2","uri":"","subtype":"PLAY","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:31+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.750Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:24:05.603+0530","ets":1462427645603,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.colours","ver":"1"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"TOUCH","extype":"","id":"next","stageid":"scene2","uri":"","subtype":"","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:31+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.750Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:24:05.838+0530","ets":1462427645838,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.colours","ver":"1"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"scene3:scene3_audio","stageid":"scene3","uri":"","subtype":"PLAY","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:31+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.750Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:24:09.573+0530","ets":1462427649573,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.colours","ver":"1"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"TOUCH","extype":"","id":"previous","stageid":"scene3","uri":"","subtype":"","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:31+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.751Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:24:09.753+0530","ets":1462427649753,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.colours","ver":"1"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"scene2:scene2_audio","stageid":"scene2","uri":"","subtype":"PLAY","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:31+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.751Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:24:09.769+0530","ets":1462427649769,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.colours","ver":"1"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"scene3:scene3_audio","stageid":"scene2","uri":"","subtype":"STOP","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:31+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.751Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:24:18.155+0530","ets":1462427658155,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.colours","ver":"1"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"TOUCH","extype":"","id":"previous","stageid":"scene2","uri":"","subtype":"","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:31+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.751Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:24:18.405+0530","ets":1462427658405,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.colours","ver":"1"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"splash:scene1_audio","stageid":"splash","uri":"","subtype":"PLAY","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:31+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.751Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:24:23.655+0530","ets":1462427663655,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.colours","ver":"1"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"TOUCH","extype":"","id":"next","stageid":"splash","uri":"","subtype":"","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:31+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.751Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:24:23.803+0530","ets":1462427663803,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.colours","ver":"1"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"scene2:scene2_audio","stageid":"scene2","uri":"","subtype":"PLAY","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:31+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.752Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:24:23.817+0530","ets":1462427663817,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.colours","ver":"1"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"splash:scene1_audio","stageid":"scene2","uri":"","subtype":"STOP","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:31+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.752Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:24:28.372+0530","ets":1462427668372,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.colours","ver":"1"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"TOUCH","extype":"","id":"next","stageid":"scene2","uri":"","subtype":"","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:31+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.752Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:24:28.607+0530","ets":1462427668607,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.colours","ver":"1"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"scene3:scene3_audio","stageid":"scene3","uri":"","subtype":"PLAY","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:31+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.752Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:24:28.618+0530","ets":1462427668618,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.colours","ver":"1"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"scene2:scene2_audio","stageid":"scene3","uri":"","subtype":"STOP","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:31+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.752Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:24:38.075+0530","ets":1462427678075,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.colours","ver":"1"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"TOUCH","extype":"","id":"next","stageid":"scene3","uri":"","subtype":"","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:31+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.752Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:24:38.299+0530","ets":1462427678299,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.colours","ver":"1"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"scene4:scene4_audio","stageid":"scene4","uri":"","subtype":"PLAY","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:31+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.752Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:24:43.005+0530","ets":1462427683005,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.colours","ver":"1"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"TOUCH","extype":"","id":"next","stageid":"scene4","uri":"","subtype":"","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:31+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.783Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:24:43.224+0530","ets":1462427683224,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.colours","ver":"1"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"scene5:scene5_audio","stageid":"scene5","uri":"","subtype":"PLAY","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:31+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.783Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:24:43.232+0530","ets":1462427683232,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.colours","ver":"1"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"scene4:scene4_audio","stageid":"scene5","uri":"","subtype":"STOP","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:31+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.784Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:24:49.640+0530","ets":1462427689640,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.colours","ver":"1"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"TOUCH","extype":"","id":"next","stageid":"scene5","uri":"","subtype":"","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:31+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.784Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:24:49.893+0530","ets":1462427689893,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.colours","ver":"1"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"scene6:scene6_audio","stageid":"scene6","uri":"","subtype":"PLAY","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:31+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.784Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:24:52.601+0530","ets":1462427692601,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.colours","ver":"1"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"TOUCH","extype":"","id":"next","stageid":"scene6","uri":"","subtype":"","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:31+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.784Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:24:52.825+0530","ets":1462427692825,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.colours","ver":"1"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"scene7:scene7_audio","stageid":"scene7","uri":"","subtype":"PLAY","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:31+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.784Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:24:52.833+0530","ets":1462427692833,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.colours","ver":"1"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"scene6:scene6_audio","stageid":"scene7","uri":"","subtype":"STOP","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:31+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.784Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:24:59.459+0530","ets":1462427699459,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.colours","ver":"1"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"TOUCH","extype":"","id":"next","stageid":"scene7","uri":"","subtype":"","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:31+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.785Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:24:59.689+0530","ets":1462427699689,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.colours","ver":"1"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"scene8:scene8_audio","stageid":"scene8","uri":"","subtype":"PLAY","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:31+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.785Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:24:59.698+0530","ets":1462427699698,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.colours","ver":"1"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"scene7:scene7_audio","stageid":"scene8","uri":"","subtype":"STOP","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:31+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.785Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:25:02.790+0530","ets":1462427702790,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.colours","ver":"1"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"TOUCH","extype":"","id":"next","stageid":"scene8","uri":"","subtype":"","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:31+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.785Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:25:02.995+0530","ets":1462427702995,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.colours","ver":"1"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"scene9:scene9_audio","stageid":"scene9","uri":"","subtype":"PLAY","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:31+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.785Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:25:03.003+0530","ets":1462427703003,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.colours","ver":"1"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"scene8:scene8_audio","stageid":"scene9","uri":"","subtype":"STOP","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:31+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.785Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:25:06.337+0530","ets":1462427706337,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.colours","ver":"1"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"TOUCH","extype":"","id":"next","stageid":"scene9","uri":"","subtype":"","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:31+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.785Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:25:06.570+0530","ets":1462427706570,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.colours","ver":"1"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"scene10:scene10_audio","stageid":"scene10","uri":"","subtype":"PLAY","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:31+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.785Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:25:06.578+0530","ets":1462427706578,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.colours","ver":"1"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"scene9:scene9_audio","stageid":"scene10","uri":"","subtype":"STOP","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:31+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.786Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:25:09.861+0530","ets":1462427709861,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.colours","ver":"1"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"TOUCH","extype":"","id":"next","stageid":"scene10","uri":"","subtype":"","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:31+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.786Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:25:10.105+0530","ets":1462427710105,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.colours","ver":"1"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"scene11:scene11_audio","stageid":"scene11","uri":"","subtype":"PLAY","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:31+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.786Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:25:10.122+0530","ets":1462427710122,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.colours","ver":"1"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"scene10:scene10_audio","stageid":"scene11","uri":"","subtype":"STOP","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:31+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.786Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:25:14.648+0530","ets":1462427714648,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.colours","ver":"1"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"TOUCH","extype":"","id":"next","stageid":"scene11","uri":"","subtype":"","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:31+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.786Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:25:14.866+0530","ets":1462427714866,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.colours","ver":"1"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"scene12:scene12_audio","stageid":"scene12","uri":"","subtype":"PLAY","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:31+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.786Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:25:14.874+0530","ets":1462427714874,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.colours","ver":"1"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"scene11:scene11_audio","stageid":"scene12","uri":"","subtype":"STOP","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:31+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.786Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:25:19.049+0530","ets":1462427719049,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.colours","ver":"1"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"TOUCH","extype":"","id":"next","stageid":"scene12","uri":"","subtype":"","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:31+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.787Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:25:24.227+0530","ets":1462427724227,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.colours","ver":"1"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"TOUCH","extype":"","id":"theme","stageid":"scene13","uri":"","subtype":"","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:31+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.787Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:25:57.576+0530","ets":1462427757576,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.elephant","ver":"20"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"TOUCH","extype":"","id":"next","stageid":"activity","uri":"","subtype":"","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:31+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.788Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:25:57.822+0530","ets":1462427757822,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.elephant","ver":"20"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"splash:titleSound","stageid":"splash","uri":"","subtype":"PLAY","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:31+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.788Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:26:04.573+0530","ets":1462427764573,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.elephant","ver":"20"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"TOUCH","extype":"","id":"next","stageid":"splash","uri":"","subtype":"","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:31+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.788Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:26:04.775+0530","ets":1462427764775,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.elephant","ver":"20"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"scene1:page1_sound","stageid":"scene1","uri":"","subtype":"PLAY","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:31+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.788Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:26:14.538+0530","ets":1462427774538,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.elephant","ver":"20"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"TOUCH","extype":"","id":"next","stageid":"scene1","uri":"","subtype":"","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:31+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.788Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:26:14.750+0530","ets":1462427774750,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.elephant","ver":"20"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"scene2:page2_sound","stageid":"scene2","uri":"","subtype":"PLAY","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:31+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.788Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:26:31.702+0530","ets":1462427791702,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.elephant","ver":"20"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"TOUCH","extype":"","id":"next","stageid":"scene2","uri":"","subtype":"","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:31+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.789Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:26:32.104+0530","ets":1462427792105,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.elephant","ver":"20"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"scene3:page3_sound","stageid":"scene3","uri":"","subtype":"PLAY","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:31+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.789Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:26:40.904+0530","ets":1462427800904,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.elephant","ver":"20"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"TOUCH","extype":"","id":"previous","stageid":"scene3","uri":"","subtype":"","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:31+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.789Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:26:41.102+0530","ets":1462427801102,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.elephant","ver":"20"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"scene2:page2_sound","stageid":"scene2","uri":"","subtype":"PLAY","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:31+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.789Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:26:42.162+0530","ets":1462427802162,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.elephant","ver":"20"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"TOUCH","extype":"","id":"previous","stageid":"scene2","uri":"","subtype":"","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:31+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.789Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:26:42.335+0530","ets":1462427802335,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.elephant","ver":"20"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"scene1:page1_sound","stageid":"scene1","uri":"","subtype":"PLAY","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:31+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.789Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:26:42.348+0530","ets":1462427802348,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.elephant","ver":"20"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"scene2:page2_sound","stageid":"scene1","uri":"","subtype":"STOP","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:31+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.789Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:26:43.072+0530","ets":1462427803072,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.elephant","ver":"20"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"TOUCH","extype":"","id":"previous","stageid":"scene1","uri":"","subtype":"","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:31+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.790Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:26:43.273+0530","ets":1462427803273,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.elephant","ver":"20"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"splash:titleSound","stageid":"splash","uri":"","subtype":"PLAY","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:31+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.790Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:26:43.285+0530","ets":1462427803285,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.elephant","ver":"20"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"scene1:page1_sound","stageid":"splash","uri":"","subtype":"STOP","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:31+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.790Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:26:55.795+0530","ets":1462427815795,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.elephant","ver":"20"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"TOUCH","extype":"","id":"next","stageid":"splash","uri":"","subtype":"","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:31+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.790Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:26:55.979+0530","ets":1462427815979,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.elephant","ver":"20"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"scene1:page1_sound","stageid":"scene1","uri":"","subtype":"PLAY","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:31+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.790Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:27:04.213+0530","ets":1462427824213,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.elephant","ver":"20"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"TOUCH","extype":"","id":"next","stageid":"scene1","uri":"","subtype":"","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:31+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.790Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:27:04.393+0530","ets":1462427824393,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.elephant","ver":"20"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"scene2:page2_sound","stageid":"scene2","uri":"","subtype":"PLAY","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:31+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.790Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:27:04.418+0530","ets":1462427824418,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.elephant","ver":"20"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"scene1:page1_sound","stageid":"scene2","uri":"","subtype":"STOP","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:31+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.790Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:27:21.576+0530","ets":1462427841576,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.elephant","ver":"20"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"TOUCH","extype":"","id":"next","stageid":"scene2","uri":"","subtype":"","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:31+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.791Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:27:21.805+0530","ets":1462427841805,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.elephant","ver":"20"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"scene3:page3_sound","stageid":"scene3","uri":"","subtype":"PLAY","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:31+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.791Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:27:22.373+0530","ets":1462427842373,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.elephant","ver":"20"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"TOUCH","extype":"","id":"next","stageid":"scene3","uri":"","subtype":"","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:31+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.791Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:27:22.638+0530","ets":1462427842638,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.elephant","ver":"20"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"scene4:page4_sound","stageid":"scene4","uri":"","subtype":"PLAY","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:31+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.791Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:27:22.653+0530","ets":1462427842653,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.elephant","ver":"20"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"scene3:page3_sound","stageid":"scene4","uri":"","subtype":"STOP","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:31+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.791Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:27:27.458+0530","ets":1462427847458,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.elephant","ver":"20"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"TOUCH","extype":"","id":"previous","stageid":"scene4","uri":"","subtype":"","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:31+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.791Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:27:27.698+0530","ets":1462427847698,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.elephant","ver":"20"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"scene3:page3_sound","stageid":"scene3","uri":"","subtype":"PLAY","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:31+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.791Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:27:27.717+0530","ets":1462427847717,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.elephant","ver":"20"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"scene4:page4_sound","stageid":"scene3","uri":"","subtype":"STOP","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:31+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.792Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:27:28.733+0530","ets":1462427848733,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.elephant","ver":"20"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"TOUCH","extype":"","id":"previous","stageid":"scene3","uri":"","subtype":"","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:31+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.792Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:27:28.932+0530","ets":1462427848932,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.elephant","ver":"20"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"scene2:page2_sound","stageid":"scene2","uri":"","subtype":"PLAY","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:31+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.792Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:27:28.948+0530","ets":1462427848948,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.elephant","ver":"20"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"scene3:page3_sound","stageid":"scene2","uri":"","subtype":"STOP","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:31+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.792Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:27:36.506+0530","ets":1462427856506,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.elephant","ver":"20"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"TOUCH","extype":"","id":"next_button","stageid":"scene2","uri":"","subtype":"","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:31+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.792Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:27:36.868+0530","ets":1462427856868,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.elephant","ver":"20"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"scene3:page3_sound","stageid":"scene3","uri":"","subtype":"PLAY","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:31+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.792Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:27:36.886+0530","ets":1462427856886,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.elephant","ver":"20"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"scene2:page2_sound","stageid":"scene3","uri":"","subtype":"STOP","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:31+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.792Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:27:44.504+0530","ets":1462427864504,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.elephant","ver":"20"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"TOUCH","extype":"","id":"next","stageid":"scene3","uri":"","subtype":"","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:31+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.793Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:27:44.729+0530","ets":1462427864729,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.elephant","ver":"20"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"scene4:page4_sound","stageid":"scene4","uri":"","subtype":"PLAY","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:31+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.793Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:27:51.771+0530","ets":1462427871771,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.elephant","ver":"20"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"TOUCH","extype":"","id":"next","stageid":"scene4","uri":"","subtype":"","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:31+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.793Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:27:52.007+0530","ets":1462427872007,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.elephant","ver":"20"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"scene5:page5_sound","stageid":"scene5","uri":"","subtype":"PLAY","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:31+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.793Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:27:59.594+0530","ets":1462427879594,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.elephant","ver":"20"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"TOUCH","extype":"","id":"next_button","stageid":"scene5","uri":"","subtype":"","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:32+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.793Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:27:59.683+0530","ets":1462427879683,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.elephant","ver":"20"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"TOUCH","extype":"","id":"next_button","stageid":"scene6","uri":"","subtype":"","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:32+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.793Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:28:00.100+0530","ets":1462427880100,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.elephant","ver":"20"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"scene6:page6_sound","stageid":"scene6","uri":"","subtype":"PLAY","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:32+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.793Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:28:05.977+0530","ets":1462427885977,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.elephant","ver":"20"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"TOUCH","extype":"","id":"next_button","stageid":"scene6","uri":"","subtype":"","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:32+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.794Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:28:08.452+0530","ets":1462427888452,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.elephant","ver":"20"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"TOUCH","extype":"","id":"endscreen_replay_button","stageid":"endScreen","uri":"","subtype":"","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:32+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.794Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:28:08.896+0530","ets":1462427888896,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.elephant","ver":"20"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"scene1:page1_sound","stageid":"scene1","uri":"","subtype":"PLAY","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:32+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.794Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:28:18.765+0530","ets":1462427898765,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.elephant","ver":"20"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"TOUCH","extype":"","id":"next_button","stageid":"scene1","uri":"","subtype":"","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:32+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.794Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:28:19.185+0530","ets":1462427899185,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.elephant","ver":"20"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"scene2:page2_sound","stageid":"scene2","uri":"","subtype":"PLAY","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:32+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.794Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:28:26.485+0530","ets":1462427906485,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.elephant","ver":"20"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"TOUCH","extype":"","id":"next_button","stageid":"scene2","uri":"","subtype":"","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:32+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.794Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:28:27.058+0530","ets":1462427907058,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.elephant","ver":"20"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"scene3:page3_sound","stageid":"scene3","uri":"","subtype":"PLAY","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:32+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.794Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:28:27.073+0530","ets":1462427907073,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.elephant","ver":"20"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"scene2:page2_sound","stageid":"scene3","uri":"","subtype":"STOP","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:32+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.795Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:28:44.519+0530","ets":1462427924519,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.elephant","ver":"20"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"TOUCH","extype":"","id":"next_button","stageid":"scene3","uri":"","subtype":"","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:32+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.795Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:28:45.073+0530","ets":1462427925073,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.elephant","ver":"20"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"scene4:page4_sound","stageid":"scene4","uri":"","subtype":"PLAY","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:32+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.795Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:28:50.724+0530","ets":1462427930724,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.elephant","ver":"20"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"TOUCH","extype":"","id":"next_button","stageid":"scene4","uri":"","subtype":"","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:32+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.795Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:28:51.290+0530","ets":1462427931290,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.elephant","ver":"20"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"scene5:page5_sound","stageid":"scene5","uri":"","subtype":"PLAY","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:32+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.795Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:28:59.575+0530","ets":1462427939575,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.elephant","ver":"20"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"TOUCH","extype":"","id":"next_button","stageid":"scene5","uri":"","subtype":"","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:32+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.795Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:29:00.129+0530","ets":1462427940129,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.elephant","ver":"20"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"scene6:page6_sound","stageid":"scene6","uri":"","subtype":"PLAY","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:32+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.795Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:29:06.619+0530","ets":1462427946619,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.elephant","ver":"20"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"TOUCH","extype":"","id":"next_button","stageid":"scene6","uri":"","subtype":"","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:32+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.796Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:29:14.408+0530","ets":1462427954408,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.elephant","ver":"20"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"TOUCH","extype":"","id":"previous","stageid":"endScreen","uri":"","subtype":"","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:32+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.796Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:29:17.770+0530","ets":1462427957770,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.elephant","ver":"20"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"TOUCH","extype":"","id":"previous","stageid":"","uri":"","subtype":"","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:32+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.796Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:29:38.132+0530","ets":1462427978132,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.elephant","ver":"20"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"TOUCH","extype":"","id":"endscreen_forward_button","stageid":"","uri":"","subtype":"","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:32+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.796Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:29:42.423+0530","ets":1462427982423,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.elephant","ver":"20"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"TOUCH","extype":"","id":"endscreen_forward_button","stageid":"","uri":"","subtype":"","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:32+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.796Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:29:44.326+0530","ets":1462427984326,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.elephant","ver":"20"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"TOUCH","extype":"","id":"endscreen_forward_button","stageid":"","uri":"","subtype":"","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:32+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.796Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:29:45.925+0530","ets":1462427985925,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.elephant","ver":"20"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"TOUCH","extype":"","id":"endscreen_forward_button","stageid":"","uri":"","subtype":"","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:32+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.796Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:29:51.195+0530","ets":1462427991195,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.elephant","ver":"20"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"TOUCH","extype":"","id":"endscreen_forward_button","stageid":"","uri":"","subtype":"","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:32+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.796Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:29:52.041+0530","ets":1462427992041,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.elephant","ver":"20"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"TOUCH","extype":"","id":"endscreen_replay_button","stageid":"","uri":"","subtype":"","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:32+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.797Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:30:09.430+0530","ets":1462428009430,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.elephant","ver":"20"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"TOUCH","extype":"","id":"endscreen_forward_button","stageid":"","uri":"","subtype":"","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:32+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.797Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:30:10.047+0530","ets":1462428010047,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.elephant","ver":"20"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"TOUCH","extype":"","id":"endscreen_forward_button","stageid":"","uri":"","subtype":"","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:32+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.797Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:30:10.363+0530","ets":1462428010363,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.elephant","ver":"20"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"TOUCH","extype":"","id":"endscreen_forward_button","stageid":"","uri":"","subtype":"","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:32+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.797Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:30:10.594+0530","ets":1462428010594,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.elephant","ver":"20"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"TOUCH","extype":"","id":"endscreen_forward_button","stageid":"","uri":"","subtype":"","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:32+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.797Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:31:04.497+0530","ets":1462428064497,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.elephant","ver":"20"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"TOUCH","extype":"","id":"next","stageid":"activity","uri":"","subtype":"","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:32+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.798Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:31:04.748+0530","ets":1462428064748,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.elephant","ver":"20"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"splash:titleSound","stageid":"splash","uri":"","subtype":"PLAY","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:32+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.798Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:31:07.612+0530","ets":1462428067612,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.elephant","ver":"20"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"TOUCH","extype":"","id":"next_button","stageid":"splash","uri":"","subtype":"","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:32+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.798Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:31:08.456+0530","ets":1462428068456,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.elephant","ver":"20"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"scene1:page1_sound","stageid":"scene1","uri":"","subtype":"PLAY","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:32+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.798Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:31:08.476+0530","ets":1462428068476,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.elephant","ver":"20"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"splash:titleSound","stageid":"scene1","uri":"","subtype":"STOP","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:32+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.799Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:31:31.106+0530","ets":1462428091106,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.elephant","ver":"20"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"TOUCH","extype":"","id":"next_button","stageid":"scene1","uri":"","subtype":"","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:32+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.799Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:31:31.890+0530","ets":1462428091890,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.elephant","ver":"20"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"scene2:page2_sound","stageid":"scene2","uri":"","subtype":"PLAY","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:32+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.799Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:31:33.242+0530","ets":1462428093242,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.elephant","ver":"20"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"TOUCH","extype":"","id":"next_button","stageid":"scene2","uri":"","subtype":"","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:32+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.799Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:31:34.185+0530","ets":1462428094185,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.elephant","ver":"20"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"scene3:page3_sound","stageid":"scene3","uri":"","subtype":"PLAY","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:32+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.799Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:31:34.204+0530","ets":1462428094204,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.elephant","ver":"20"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"scene2:page2_sound","stageid":"scene3","uri":"","subtype":"STOP","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:32+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.799Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:31:35.299+0530","ets":1462428095299,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.elephant","ver":"20"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"TOUCH","extype":"","id":"next_button","stageid":"scene3","uri":"","subtype":"","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:32+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.800Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:31:35.886+0530","ets":1462428095886,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.elephant","ver":"20"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"scene4:page4_sound","stageid":"scene4","uri":"","subtype":"PLAY","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:32+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.800Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:31:35.907+0530","ets":1462428095907,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.elephant","ver":"20"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"scene3:page3_sound","stageid":"scene4","uri":"","subtype":"STOP","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:32+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.800Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:31:37.000+0530","ets":1462428097000,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.elephant","ver":"20"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"TOUCH","extype":"","id":"next_button","stageid":"scene4","uri":"","subtype":"","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:32+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.800Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:31:37.569+0530","ets":1462428097569,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.elephant","ver":"20"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"scene5:page5_sound","stageid":"scene5","uri":"","subtype":"PLAY","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:32+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.800Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:31:37.588+0530","ets":1462428097588,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.elephant","ver":"20"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"scene4:page4_sound","stageid":"scene5","uri":"","subtype":"STOP","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:32+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.800Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:31:43.858+0530","ets":1462428103858,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.elephant","ver":"20"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"TOUCH","extype":"","id":"next_button","stageid":"scene5","uri":"","subtype":"","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:32+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.800Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:31:44.361+0530","ets":1462428104361,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.elephant","ver":"20"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"scene6:page6_sound","stageid":"scene6","uri":"","subtype":"PLAY","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:32+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.800Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:31:44.373+0530","ets":1462428104373,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.elephant","ver":"20"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"scene5:page5_sound","stageid":"scene6","uri":"","subtype":"STOP","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:32+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.801Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:31:46.884+0530","ets":1462428106884,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.elephant","ver":"20"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"TOUCH","extype":"","id":"next_button","stageid":"scene6","uri":"","subtype":"","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:32+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.801Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:31:48.615+0530","ets":1462428108615,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.elephant","ver":"20"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"TOUCH","extype":"","id":"endscreen_forward_button","stageid":"endScreen","uri":"","subtype":"","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:32+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.801Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:31:49.394+0530","ets":1462428109394,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.elephant","ver":"20"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"TOUCH","extype":"","id":"next","stageid":"worksheet","uri":"","subtype":"","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:32+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.801Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:31:49.667+0530","ets":1462428109667,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.elephant","ver":"20"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"worksheet:mtf_bear_sound","stageid":"worksheet","uri":"","subtype":"PLAY","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:32+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.801Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:32:00.052+0530","ets":1462428120052,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.elephant","ver":"20"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"CHOOSE","extype":"","id":"bear","stageid":"worksheet","uri":"","subtype":"","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:32+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.801Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:32:01.648+0530","ets":1462428121648,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.elephant","ver":"20"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"CHOOSE","extype":"","id":"bear","stageid":"worksheet","uri":"","subtype":"","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:32+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.802Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:32:02.493+0530","ets":1462428122493,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.elephant","ver":"20"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"CHOOSE","extype":"","id":"bear","stageid":"worksheet","uri":"","subtype":"","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:32+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.802Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:32:04.145+0530","ets":1462428124145,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.elephant","ver":"20"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"worksheet:goodJobSound","stageid":"worksheet","uri":"","subtype":"PLAY","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:32+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.802Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:32:04.151+0530","ets":1462428124151,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.elephant","ver":"20"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"SHOW","extype":"","id":"goodjobDialog","stageid":"worksheet","uri":"","subtype":"","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:32+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.802Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:32:04.723+0530","ets":1462428124723,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.elephant","ver":"20"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"worksheet:goodJobSound","stageid":"worksheet","uri":"","subtype":"PLAY","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:32+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.802Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:32:11.252+0530","ets":1462428131252,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.elephant","ver":"20"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"worksheet:goodJobSound","stageid":"worksheet","uri":"","subtype":"PLAY","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:32+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.803Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:32:11.974+0530","ets":1462428131974,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.elephant","ver":"20"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"TOUCH","extype":"","id":"next","stageid":"worksheet","uri":"","subtype":"","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:32+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.803Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:32:12.316+0530","ets":1462428132316,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.elephant","ver":"20"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"worksheet:mtf_lion_sound","stageid":"worksheet","uri":"","subtype":"PLAY","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:32+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.803Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:32:12.661+0530","ets":1462428132661,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.elephant","ver":"20"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"TOUCH","extype":"","id":"next","stageid":"worksheet","uri":"","subtype":"","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:32+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.803Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:32:12.987+0530","ets":1462428132987,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.elephant","ver":"20"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"worksheet:mtf_yummy_sound","stageid":"worksheet","uri":"","subtype":"PLAY","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:32+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.803Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:32:17.896+0530","ets":1462428137896,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.elephant","ver":"20"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"CHOOSE","extype":"","id":"enjoyingfood","stageid":"worksheet","uri":"","subtype":"","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:32+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.803Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:32:19.326+0530","ets":1462428139326,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.elephant","ver":"20"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"TOUCH","extype":"","id":"next","stageid":"worksheet","uri":"","subtype":"","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:32+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.803Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:32:19.585+0530","ets":1462428139585,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.elephant","ver":"20"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"worksheet:fitb_sound1","stageid":"worksheet","uri":"","subtype":"PLAY","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:32+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.803Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:32:29.833+0530","ets":1462428149833,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.elephant","ver":"20"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"CHOOSE","extype":"","id":"Saree","stageid":"worksheet","uri":"","subtype":"","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:32+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.804Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:32:31.347+0530","ets":1462428151347,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.elephant","ver":"20"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"TOUCH","extype":"","id":"next","stageid":"worksheet","uri":"","subtype":"","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:32+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.804Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:32:31.607+0530","ets":1462428151607,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.elephant","ver":"20"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"worksheet:fitb_sound2","stageid":"worksheet","uri":"","subtype":"PLAY","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:32+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.804Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:32:31.980+0530","ets":1462428151980,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.elephant","ver":"20"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"TOUCH","extype":"","id":"next","stageid":"worksheet","uri":"","subtype":"","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:32+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.804Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:32:32.226+0530","ets":1462428152227,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.elephant","ver":"20"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"worksheet:fitb_sound3","stageid":"worksheet","uri":"","subtype":"PLAY","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:32+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.804Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:32:34.910+0530","ets":1462428154910,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.elephant","ver":"20"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"TOUCH","extype":"","id":"previous","stageid":"worksheet","uri":"","subtype":"","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:32+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.804Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:32:34.994+0530","ets":1462428154994,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.elephant","ver":"20"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"worksheet:fitb_sound2","stageid":"worksheet","uri":"","subtype":"STOP","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:32+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.804Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:32:35.109+0530","ets":1462428155109,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.elephant","ver":"20"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"worksheet:fitb_sound2","stageid":"worksheet","uri":"","subtype":"PLAY","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:32+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.804Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:32:42.790+0530","ets":1462428162790,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.elephant","ver":"20"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"CHOOSE","extype":"","id":"Trumpet","stageid":"worksheet","uri":"","subtype":"","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:32+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.804Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:32:45.452+0530","ets":1462428165452,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.elephant","ver":"20"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"worksheet:goodJobSound","stageid":"worksheet","uri":"","subtype":"PLAY","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:32+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.805Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:32:45.464+0530","ets":1462428165464,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.elephant","ver":"20"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"SHOW","extype":"","id":"goodjobDialog","stageid":"worksheet","uri":"","subtype":"","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:32+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.805Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:32:48.126+0530","ets":1462428168126,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.elephant","ver":"20"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"TOUCH","extype":"","id":"next","stageid":"worksheet","uri":"","subtype":"","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:32+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.805Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:32:48.212+0530","ets":1462428168212,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.elephant","ver":"20"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"worksheet:fitb_sound3","stageid":"worksheet","uri":"","subtype":"STOP","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:32+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.805Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:32:48.390+0530","ets":1462428168390,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.elephant","ver":"20"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"worksheet:fitb_sound3","stageid":"worksheet","uri":"","subtype":"PLAY","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:32+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.805Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:32:48.646+0530","ets":1462428168646,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.elephant","ver":"20"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"TOUCH","extype":"","id":"next","stageid":"worksheet","uri":"","subtype":"","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:32+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.805Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:32:48.843+0530","ets":1462428168843,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.elephant","ver":"20"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"worksheet:fitb_sound4","stageid":"worksheet","uri":"","subtype":"PLAY","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:32+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.805Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:32:58.333+0530","ets":1462428178333,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.elephant","ver":"20"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"CHOOSE","extype":"","id":"Jungle King","stageid":"worksheet","uri":"","subtype":"","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:32+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.806Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:32:59.324+0530","ets":1462428179324,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.elephant","ver":"20"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"TOUCH","extype":"","id":"next","stageid":"worksheet","uri":"","subtype":"","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:32+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.806Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:33:03.724+0530","ets":1462428183724,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.elephant","ver":"20"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"TOUCH","extype":"","id":"endscreen_forward_button","stageid":"endScreen2","uri":"","subtype":"","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:32+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.806Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:33:04.191+0530","ets":1462428184191,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.elephant","ver":"20"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"worksheet2:tickWord","stageid":"worksheet2","uri":"","subtype":"PLAY","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:32+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.806Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:33:07.968+0530","ets":1462428187968,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.elephant","ver":"20"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"CHOOSE","extype":"","id":"Lamp ","stageid":"worksheet2","uri":"","subtype":"","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:32+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.806Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:33:08.939+0530","ets":1462428188939,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.elephant","ver":"20"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"TOUCH","extype":"","id":"next","stageid":"worksheet2","uri":"","subtype":"","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:32+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.806Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:33:09.139+0530","ets":1462428189139,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.elephant","ver":"20"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"worksheet2:tickWord","stageid":"worksheet2","uri":"","subtype":"PLAY","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:32+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.807Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:33:11.077+0530","ets":1462428191077,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.elephant","ver":"20"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"CHOOSE","extype":"","id":"Celebrating","stageid":"worksheet2","uri":"","subtype":"","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:32+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.807Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:33:11.688+0530","ets":1462428191688,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.elephant","ver":"20"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"TOUCH","extype":"","id":"next","stageid":"worksheet2","uri":"","subtype":"","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:32+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.807Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:33:11.765+0530","ets":1462428191765,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.elephant","ver":"20"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"worksheet2:tickWord","stageid":"worksheet2","uri":"","subtype":"STOP","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:32+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.807Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:33:11.917+0530","ets":1462428191917,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.elephant","ver":"20"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"worksheet2:tickWord","stageid":"worksheet2","uri":"","subtype":"PLAY","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:32+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.807Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:33:14.930+0530","ets":1462428194930,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.elephant","ver":"20"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"CHOOSE","extype":"","id":"Trumpet ","stageid":"worksheet2","uri":"","subtype":"","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:32+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.807Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:33:15.585+0530","ets":1462428195585,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.elephant","ver":"20"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"TOUCH","extype":"","id":"next","stageid":"worksheet2","uri":"","subtype":"","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:32+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.807Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:33:15.799+0530","ets":1462428195799,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.elephant","ver":"20"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"worksheet2:tickWord","stageid":"worksheet2","uri":"","subtype":"PLAY","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:32+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.807Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:33:23.483+0530","ets":1462428203483,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.elephant","ver":"20"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"CHOOSE","extype":"","id":"Played ","stageid":"worksheet2","uri":"","subtype":"","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:32+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.808Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:33:23.853+0530","ets":1462428203853,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.elephant","ver":"20"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"CHOOSE","extype":"","id":"Played ","stageid":"worksheet2","uri":"","subtype":"","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:32+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.808Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:33:26.657+0530","ets":1462428206657,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.elephant","ver":"20"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"CHOOSE","extype":"","id":"Played ","stageid":"worksheet2","uri":"","subtype":"","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:32+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.808Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:33:27.523+0530","ets":1462428207523,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.elephant","ver":"20"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"worksheet2:goodJobSound","stageid":"worksheet2","uri":"","subtype":"PLAY","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:32+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.808Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:33:27.527+0530","ets":1462428207527,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.elephant","ver":"20"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"SHOW","extype":"","id":"goodjobDialog","stageid":"worksheet2","uri":"","subtype":"","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:32+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.808Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:33:31.051+0530","ets":1462428211051,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.elephant","ver":"20"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"TOUCH","extype":"","id":"goodjob_forward_button","stageid":"worksheet2","uri":"","subtype":"","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:32+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.808Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:33:31.501+0530","ets":1462428211501,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.elephant","ver":"20"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"worksheet2:taponP","stageid":"worksheet2","uri":"","subtype":"PLAY","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:32+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.809Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:33:38.487+0530","ets":1462428218487,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.elephant","ver":"20"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"CHOOSE","extype":"","id":"P","stageid":"worksheet2","uri":"","subtype":"","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:32+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.809Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:33:40.289+0530","ets":1462428220289,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.elephant","ver":"20"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"TOUCH","extype":"","id":"next","stageid":"worksheet2","uri":"","subtype":"","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:32+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.809Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:33:40.524+0530","ets":1462428220524,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.elephant","ver":"20"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"worksheet2:taponP","stageid":"worksheet2","uri":"","subtype":"PLAY","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:32+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.809Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:33:51.665+0530","ets":1462428231665,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.elephant","ver":"20"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"CHOOSE","extype":"","id":"P","stageid":"worksheet2","uri":"","subtype":"","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:32+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.809Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:33:56.887+0530","ets":1462428236887,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.elephant","ver":"20"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"TOUCH","extype":"","id":"next","stageid":"worksheet2","uri":"","subtype":"","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:32+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.809Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:33:57.097+0530","ets":1462428237097,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.elephant","ver":"20"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"worksheet2:taponP","stageid":"worksheet2","uri":"","subtype":"PLAY","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:32+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.809Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:33:57.642+0530","ets":1462428237642,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.elephant","ver":"20"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"TOUCH","extype":"","id":"next","stageid":"worksheet2","uri":"","subtype":"","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:32+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.810Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:33:57.705+0530","ets":1462428237705,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.elephant","ver":"20"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"worksheet2:taponP","stageid":"worksheet2","uri":"","subtype":"STOP","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:32+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.810Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:33:57.846+0530","ets":1462428237846,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.elephant","ver":"20"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"worksheet2:taponP","stageid":"worksheet2","uri":"","subtype":"PLAY","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:32+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.810Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:34:00.278+0530","ets":1462428240278,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.elephant","ver":"20"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"TOUCH","extype":"","id":"previous","stageid":"worksheet2","uri":"","subtype":"","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:32+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.810Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:34:00.349+0530","ets":1462428240349,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.elephant","ver":"20"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"worksheet2:taponP","stageid":"worksheet2","uri":"","subtype":"STOP","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:32+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.810Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:34:00.514+0530","ets":1462428240514,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.elephant","ver":"20"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"worksheet2:taponP","stageid":"worksheet2","uri":"","subtype":"PLAY","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:32+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.810Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:34:02.609+0530","ets":1462428242609,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.elephant","ver":"20"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"CHOOSE","extype":"","id":"P","stageid":"worksheet2","uri":"","subtype":"","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:32+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.810Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:34:04.802+0530","ets":1462428244802,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.elephant","ver":"20"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"TOUCH","extype":"","id":"next","stageid":"worksheet2","uri":"","subtype":"","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:32+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.810Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:34:05.010+0530","ets":1462428245010,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.elephant","ver":"20"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"worksheet2:taponP","stageid":"worksheet2","uri":"","subtype":"PLAY","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:32+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.811Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:34:07.629+0530","ets":1462428247629,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.elephant","ver":"20"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"CHOOSE","extype":"","id":"P","stageid":"worksheet2","uri":"","subtype":"","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:32+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.811Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:34:09.748+0530","ets":1462428249748,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.elephant","ver":"20"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"TOUCH","extype":"","id":"next","stageid":"worksheet2","uri":"","subtype":"","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:32+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.811Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:34:11.604+0530","ets":1462428251604,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.elephant","ver":"20"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"TOUCH","extype":"","id":"next","stageid":"endScreen3","uri":"","subtype":"","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:32+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.811Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:34:28.832+0530","ets":1462428268832,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.elephant","ver":"20"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"TOUCH","extype":"","id":"next","stageid":"activity","uri":"","subtype":"","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:32+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.812Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:34:29.091+0530","ets":1462428269091,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.elephant","ver":"20"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"splash:titleSound","stageid":"splash","uri":"","subtype":"PLAY","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:32+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.812Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:34:29.865+0530","ets":1462428269865,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.elephant","ver":"20"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"TOUCH","extype":"","id":"next","stageid":"splash","uri":"","subtype":"","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:32+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.812Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:34:30.023+0530","ets":1462428270023,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.elephant","ver":"20"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"scene1:page1_sound","stageid":"scene1","uri":"","subtype":"PLAY","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:32+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.812Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:34:30.055+0530","ets":1462428270055,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.elephant","ver":"20"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"splash:titleSound","stageid":"scene1","uri":"","subtype":"STOP","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:32+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.812Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:34:31.527+0530","ets":1462428271527,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.elephant","ver":"20"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"TOUCH","extype":"","id":"next","stageid":"scene1","uri":"","subtype":"","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:32+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.812Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:34:31.738+0530","ets":1462428271738,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.elephant","ver":"20"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"scene2:page2_sound","stageid":"scene2","uri":"","subtype":"PLAY","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:32+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.813Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:34:31.750+0530","ets":1462428271750,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.elephant","ver":"20"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"scene1:page1_sound","stageid":"scene2","uri":"","subtype":"STOP","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:32+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.813Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:34:33.085+0530","ets":1462428273085,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.elephant","ver":"20"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"TOUCH","extype":"","id":"next","stageid":"scene2","uri":"","subtype":"","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:32+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.813Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:34:33.285+0530","ets":1462428273285,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.elephant","ver":"20"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"scene3:page3_sound","stageid":"scene3","uri":"","subtype":"PLAY","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:32+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.813Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:34:33.312+0530","ets":1462428273312,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.elephant","ver":"20"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"scene2:page2_sound","stageid":"scene3","uri":"","subtype":"STOP","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:32+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.813Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:34:56.470+0530","ets":1462428296470,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.elephant","ver":"20"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"TOUCH","extype":"","id":"story_audio_button","stageid":"scene3","uri":"","subtype":"","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:32+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.814Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:34:56.495+0530","ets":1462428296495,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.elephant","ver":"20"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"scene3:page3_sound","stageid":"scene3","uri":"","subtype":"PLAY","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:32+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.814Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:34:56.872+0530","ets":1462428296872,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.elephant","ver":"20"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"TOUCH","extype":"","id":"story_audio_button","stageid":"scene3","uri":"","subtype":"","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:32+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.814Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:34:56.890+0530","ets":1462428296890,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.elephant","ver":"20"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"scene3:page3_sound","stageid":"scene3","uri":"","subtype":"PAUSE","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:32+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.814Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:34:57.144+0530","ets":1462428297144,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.elephant","ver":"20"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"TOUCH","extype":"","id":"story_audio_button","stageid":"scene3","uri":"","subtype":"","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:32+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.814Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:34:57.156+0530","ets":1462428297156,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.elephant","ver":"20"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"scene3:page3_sound","stageid":"scene3","uri":"","subtype":"PLAY","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:32+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.814Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:34:57.336+0530","ets":1462428297336,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.elephant","ver":"20"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"TOUCH","extype":"","id":"story_audio_button","stageid":"scene3","uri":"","subtype":"","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:32+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.814Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:34:57.344+0530","ets":1462428297344,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.elephant","ver":"20"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"scene3:page3_sound","stageid":"scene3","uri":"","subtype":"PAUSE","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:32+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.814Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:34:57.594+0530","ets":1462428297594,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.elephant","ver":"20"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"TOUCH","extype":"","id":"story_audio_button","stageid":"scene3","uri":"","subtype":"","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:32+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.814Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:34:57.600+0530","ets":1462428297600,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.elephant","ver":"20"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"scene3:page3_sound","stageid":"scene3","uri":"","subtype":"PLAY","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:32+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.814Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:35:01.225+0530","ets":1462428301225,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.elephant","ver":"20"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"TOUCH","extype":"","id":"next","stageid":"scene3","uri":"","subtype":"","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:32+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.815Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:35:01.596+0530","ets":1462428301596,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.elephant","ver":"20"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"scene4:page4_sound","stageid":"scene4","uri":"","subtype":"PLAY","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:32+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.815Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:35:01.612+0530","ets":1462428301612,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.elephant","ver":"20"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"scene3:page3_sound","stageid":"scene4","uri":"","subtype":"STOP","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:32+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.815Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:35:07.135+0530","ets":1462428307135,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.elephant","ver":"20"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"TOUCH","extype":"","id":"next","stageid":"scene4","uri":"","subtype":"","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:32+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.815Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:35:07.432+0530","ets":1462428307432,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.elephant","ver":"20"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"scene5:page5_sound","stageid":"scene5","uri":"","subtype":"PLAY","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:32+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.815Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:35:08.442+0530","ets":1462428308442,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.elephant","ver":"20"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"TOUCH","extype":"","id":"next","stageid":"scene5","uri":"","subtype":"","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:32+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.815Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:35:08.625+0530","ets":1462428308625,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.elephant","ver":"20"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"scene6:page6_sound","stageid":"scene6","uri":"","subtype":"PLAY","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:32+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.815Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:35:08.637+0530","ets":1462428308637,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.elephant","ver":"20"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"LISTEN","extype":"","id":"scene5:page5_sound","stageid":"scene6","uri":"","subtype":"STOP","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:32+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.815Z"} -{"eid":"OE_INTERACT","ts":"2016-05-05T11:35:10.088+0530","ets":1462428310088,"ver":"2.0","gdata":{"id":"org.ekstep.story.en.elephant","ver":"20"},"sid":"2b927be8-6a74-460b-aa20-0c991bcf57f6","uid":"40550853-c88c-4f6b-8d33-88d0f47c32f4","did":"d601e461a64b06f8828886e2f740e1688491a0a8","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"TOUCH","extype":"","id":"next","stageid":"scene6","uri":"","subtype":"","pos":[],"values":[],"tid":"","rating":0.0}},"tags":[{"genie":["becb887fe82f24c644482eb30041da6d88bd8150"]}],"metadata":{"sync_timestamp":"2016-11-19T23:12:33+00:00","public":"true"},"@timestamp":"2016-11-09T08:16:35.816Z"} diff --git a/src/test/resources/application.conf b/src/test/resources/application.conf deleted file mode 100644 index bfb1b9d..0000000 --- a/src/test/resources/application.conf +++ /dev/null @@ -1,21 +0,0 @@ -local_extract_path="/tmp/extract/" -cloud_storage_type="aws" - -# AWS S3 Configuration -aws_storage_container="ekstep-dev-data-store" -aws_storage_key="" -aws_storage_secret="" - -# Azure configuration -azure_storage_container="test-container" - -#Gcloud configuration -gcloud_storage_container="test-obsrv-data-store" - -# OCI Configuration -oci_storage_container="" -oci_storage_key="" -oci_storage_secret="" -oci_region="ap-hyderabad-1" -# sample endpoint url: https://.compat.objectstorage..oraclecloud.com -oci_storage_endpoint="https://xyz.compat.objectstorage.ap-hyderabad-1.oraclecloud.com" diff --git a/src/test/resources/test-data.log b/src/test/resources/test-data.log deleted file mode 100644 index fe67b2a..0000000 --- a/src/test/resources/test-data.log +++ /dev/null @@ -1,18 +0,0 @@ -{"tags":[],"uid":"","sid":"","ts":"2016-05-21T15:42:47+05:30","edata":{"eks":{"dspec":{"camera":",","cap":[],"cpu":"abi: armeabi-v7a ARMv7 Processor rev 4 (v7l) ","dlocname":"","dname":"","edisk":12.95,"id":"1ea46b9d75036b5d","idisk":294.54,"make":"PANASONIC Panasonic P55 Novo","mem":-1.0,"os":"Android 4.4.2","scrn":4.59,"sims":1.0},"loc":""}},"did":"3418a40a297e46e8f61037a438e6985da39ca328","ver":"1.0","type":"events","eid":"GE_GENIE_START","@version":"1","gdata":{"id":"genie.android","ver":"4.2.61.4"},"@timestamp":"2016-05-25T08:45:52.493Z","uuid":"49efdece-e843-4725-a824-0796bfb8c97e3","key":"","metadata":{"checksum":"e478aba8a2096685e555d3b587fdf5361d5acc08"}} -{"tags":[],"uid":"","sid":"","ts":"2016-05-21T16:34:08+05:30","edata":{"eks":{"length":3080.0}},"did":"3418a40a297e46e8f61037a438e6985da39ca328","ver":"1.0","type":"events","eid":"GE_GENIE_END","@version":"1","gdata":{"id":"genie.android","ver":"4.2.61.4"},"@timestamp":"2016-05-25T08:45:52.532Z","uuid":"49efdece-e843-4725-a824-0796bfb8c97e95","key":"","metadata":{"checksum":"767e8e1b879b187ad615cc1aeaf8e7a0ca790e15"}} -{"tags":[],"uid":"","sid":"","ts":"2016-05-21T16:34:08+05:30","edata":{"eks":{"length":3080.0}},"did":"3418a40a297e46e8f61037a438e6985da39ca321","ver":"1.0","type":"events","eid":"GE_GENIE_END","@version":"1","gdata":{"id":"genie.android","ver":"4.2.61.4"},"@timestamp":"2016-05-25T08:45:52.532Z","uuid":"49efdece-e843-4725-a824-0796bfb8c97e95","key":"","metadata":{"checksum":"767e8e1b879b187ad615cc1aeaf8e7a0ca790e15"}} -{"tags":[],"eid":"GE_GENIE_START","ts":"2016-03-12T19:14:45+05:30","ets":0,"@timestamp":"2016-04-25T06:38:30.361Z","ver":"1.0","gdata":{"id":"genie.android","ver":"4.2.78.6"},"sid":"70307295-4fbb-49a2-aceb-561625d7855c","uid":"927defc9-4da1-40ad-a780-8c92bf070ab8","did":"3fdf3dfc1f004ffa3762fdadded8f44208c8d06c","edata":{"eks":{"dspec":{"dlocname":"","dname":"","scrn":4.34,"camera":"5.0,1.0","idisk":217.95,"os":"Android 4.4.2","id":"4279a476a5040f3e","sims":1.0,"mem":-1.0,"cpu":"abi: armeabi-v7a ARMv7 Processor rev 4 (v7l) ","edisk":2.18,"make":"Samsung SM-G355H","cap":[]},"loc":"","score":0,"atmpts":0,"failedatmpts":0}}} -{"tags":[],"eid":"GE_GAME_END","ts":"2016-03-12T19:14:46+05:30","ets":0,"@timestamp":"2016-04-25T06:38:30.361Z","ver":"1.0","gdata":{"id":"genie.android","ver":"4.2.78.6"},"sid":"70307295-4fbb-49a2-aceb-561625d7855c","uid":"927defc9-4da1-40ad-a780-8c92bf070ab8","did":"3fdf3dfc1f004ffa3762fdadded8f44208c8d06c","edata":{"eks":{"score":0,"length":261.0,"atmpts":0,"failedatmpts":0,"gid":"org.ekstep.num.scrn.basic"}}} -{"tags":[],"eid":"GE_GENIE_END","ts":"2016-03-12T19:14:50+05:30","ets":0,"@timestamp":"2016-04-25T06:38:30.361Z","ver":"1.0","gdata":{"id":"genie.android","ver":"4.2.78.6"},"sid":"70307295-4fbb-49a2-aceb-561625d7855c","uid":"927defc9-4da1-40ad-a780-8c92bf070ab8","did":"3fdf3dfc1f004ffa3762fdadded8f44208c8d06c","edata":{"eks":{"score":0,"length":4.0,"atmpts":0,"failedatmpts":0}}} -{"tags":[],"eid":"GE_INTERRUPT","ts":"2016-03-12T19:14:53+05:30","ets":0,"@timestamp":"2016-04-25T06:38:30.361Z","ver":"1.0","gdata":{"id":"genie.android","ver":"4.2.78.6"},"sid":"70307295-4fbb-49a2-aceb-561625d7855c","uid":"927defc9-4da1-40ad-a780-8c92bf070ab8","did":"3fdf3dfc1f004ffa3762fdadded8f44208c8d06c","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"BACKGROUND","id":""},"ext":{}}} -{"tags":[],"eid":"GE_INTERRUPT","ts":"2016-03-12T20:33:01+05:30","ets":0,"@timestamp":"2016-04-25T06:38:30.362Z","ver":"1.0","gdata":{"id":"genie.android","ver":"4.2.78.6"},"sid":"70307295-4fbb-49a2-aceb-561625d7855c","uid":"927defc9-4da1-40ad-a780-8c92bf070ab8","did":"3fdf3dfc1f004ffa3762fdadded8f44208c8d06c","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"BACKGROUND","id":""},"ext":{}}} -{"tags":[],"eid":"GE_INTERRUPT","ts":"2016-03-13T18:52:58+05:30","ets":0,"@timestamp":"2016-04-25T06:38:30.362Z","ver":"1.0","gdata":{"id":"genie.android","ver":"4.2.78.6"},"sid":"70307295-4fbb-49a2-aceb-561625d7855c","uid":"927defc9-4da1-40ad-a780-8c92bf070ab8","did":"3fdf3dfc1f004ffa3762fdadded8f44208c8d06c","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"BACKGROUND","id":""},"ext":{}}} -{"tags":[],"eid":"GE_GENIE_RESUME","ts":"2016-03-13T18:54:45+05:30","ets":0,"@timestamp":"2016-04-25T06:38:30.362Z","ver":"1.0","gdata":{"id":"genie.android","ver":"4.2.78.6"},"sid":"70307295-4fbb-49a2-aceb-561625d7855c","uid":"927defc9-4da1-40ad-a780-8c92bf070ab8","did":"3fdf3dfc1f004ffa3762fdadded8f44208c8d06c","edata":{"eks":{"loc":"","score":0,"atmpts":0,"failedatmpts":0}}} -{"tags":[],"eid":"GE_INTERRUPT","ts":"2016-03-13T18:55:39+05:30","ets":0,"@timestamp":"2016-04-25T06:38:30.362Z","ver":"1.0","gdata":{"id":"genie.android","ver":"4.2.78.6"},"sid":"70307295-4fbb-49a2-aceb-561625d7855c","uid":"927defc9-4da1-40ad-a780-8c92bf070ab8","did":"3fdf3dfc1f004ffa3762fdadded8f44208c8d06c","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"BACKGROUND","id":""},"ext":{}}} -{"tags":[],"eid":"GE_GENIE_RESUME","ts":"2016-03-13T18:55:59+05:30","ets":0,"@timestamp":"2016-04-25T06:38:30.362Z","ver":"1.0","gdata":{"id":"genie.android","ver":"4.2.78.6"},"sid":"70307295-4fbb-49a2-aceb-561625d7855c","uid":"927defc9-4da1-40ad-a780-8c92bf070ab8","did":"3fdf3dfc1f004ffa3762fdadded8f44208c8d06c","edata":{"eks":{"loc":"","score":0,"atmpts":0,"failedatmpts":0}}} -{"tags":[],"eid":"GE_INTERRUPT","ts":"2016-03-13T18:56:24+05:30","ets":0,"@timestamp":"2016-04-25T06:38:30.363Z","ver":"1.0","gdata":{"id":"genie.android","ver":"4.2.78.6"},"sid":"70307295-4fbb-49a2-aceb-561625d7855c","uid":"927defc9-4da1-40ad-a780-8c92bf070ab8","did":"3fdf3dfc1f004ffa3762fdadded8f44208c8d06c","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"BACKGROUND","id":""},"ext":{}}} -{"tags":[{"partnerid":"org.ekstep.partner.pratham"}],"eid":"GE_ERROR","ts":"2016-03-17T13:22:27+05:30","ets":0,"@timestamp":"2016-04-25T05:29:05.357Z","ver":"1.0","gdata":{"id":"genieservice.android","ver":"1.0.229-production"},"sid":"","uid":"","did":"5c7567479e8515e740eaa2d21157f610bf057831","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"type":"GENIESERVICES","id":"getCurrentUser"}}} -{"tags":[{"partnerid":"org.ekstep.partner.pratham"}],"eid":"GE_SERVICE_API_CALL","ts":"2016-03-17T13:22:27+05:30","ets":0,"@timestamp":"2016-04-25T05:29:05.358Z","ver":"1.0","gdata":{"id":"genieservice.android","ver":"1.0.229-production"},"sid":"","uid":"","did":"5c7567479e8515e740eaa2d21157f610bf057831","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0}}} -{"tags":[{"partnerid":"org.ekstep.partner.pratham"}],"eid":"GE_GENIE_START","ts":"2016-03-17T13:22:30+05:30","ets":0,"@timestamp":"2016-04-25T05:29:05.358Z","ver":"1.0","gdata":{"id":"genie.android","ver":"4.2.78.6"},"sid":"","uid":"","did":"5c7567479e8515e740eaa2d21157f610bf057831","edata":{"eks":{"dspec":{"dlocname":"","dname":"","scrn":6.99,"camera":"4.0,2.0","idisk":1.01,"os":"Android 4.4.2","id":"740487ab0a9a663f","sims":-1.0,"mem":-1.0,"cpu":"abi: armeabi-v7a ARMv7 Processor rev 3 (v7l) ","edisk":4.85,"make":"Samsung SM-T231","cap":[]},"loc":"","score":0,"atmpts":0,"failedatmpts":0}}} -{"tags":[{"partnerid":"org.ekstep.partner.pratham"}],"eid":"GE_SESSION_START","ts":"2016-03-17T13:22:41+05:30","ets":0,"@timestamp":"2016-04-25T05:29:05.358Z","ver":"1.0","gdata":{"id":"genieservice.android","ver":"1.0.229-production"},"sid":"4e46572f-4d0b-4e96-a1a1-ec8c52e1e6b6","uid":"6eddac9b-ccdb-47aa-be06-a44b06f7fe62","did":"5c7567479e8515e740eaa2d21157f610bf057831","edata":{"eks":{"loc":"","score":0,"atmpts":0,"failedatmpts":0}}} -{"tags":[{"partnerid":"org.ekstep.partner.pratham"}],"eid":"GE_LAUNCH_GAME","ts":"2016-03-17T13:23:41+05:30","ets":0,"@timestamp":"2016-04-25T05:29:05.358Z","ver":"1.0","gdata":{"id":"genie.android","ver":"4.2.78.6"},"sid":"4e46572f-4d0b-4e96-a1a1-ec8c52e1e6b6","uid":"6eddac9b-ccdb-47aa-be06-a44b06f7fe62","did":"5c7567479e8515e740eaa2d21157f610bf057831","edata":{"eks":{"score":0,"atmpts":0,"failedatmpts":0,"gid":"org.ekstep.num.scrn.basic"}}} \ No newline at end of file diff --git a/src/test/resources/test-extract.zip b/src/test/resources/test-extract.zip deleted file mode 100644 index afbbc434515e243a88d827b013cb7762829224ee..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1444 zcmZ9Mc{~#g7{|vPa~~N|jydL*a#keQ#HhJ8LQ;n0m?1fO9j$3O*GBYWvU=TWP430Y ziwN&XJ z;Gno5qloBGZy#|0Q3>Uz2SWL+^T`3YMeyiJOX=Q~Z@S{8S7?P@oA#ASbeK}ayg(B@ z(@37rG1p&%7}@ujrQVk?OH?IH_7F(NrYku*m$c;6GB;9% znZ+5GoO9Gg6AA{yg3JW9y+W=R;%Z&-Rm*|d7m%7BrqpKvW23`rs8LT^g!eUtNF^&=_JCk4itG9D*U3R%j{ zhw>&%NgL>gv+NDpH;JKQ$zE!b%C2%f5}T8$TU)knFS z+YX#KT%BJXOn9_oJyc?N(cY&Ic=aX|q#`yLyd=KwFbj!J46)koi`fFFoaNUm$6hAw z2!BS6`no4;HO3U1c+IiA0Jq^nF(Yn<|hzk|h6iS^8TB2n|3fta`6o{_TNm)peE+$>oy`+Yw6 z%8PM)maKwlUS5Y48;K)MoJVw5;XX!=BgV7`AwnC)ckAd3U@Hg%Z0inDZL6rOeb$2_ zGKtGANCPP&0T5InLuTGht>30_u?bokkDseZq)2K($q(c?bq?$5iPlBIg~Vr#@pk&! z1hu1?VH)pJ3Mg}9A+^ge9rY)$Xb%mqA`|ZD45waL2n>Z!IqX9rHFP>V?$))DoEK}P z8g52e@E{%ZWzxbo+SEw}`Bg#{p(vtWx zkilv;XNeOX$YaCh}HA~AHO9c-LZO`Pq3i6JmHC;D{;! vfPnMygMt5v^w*mNK>Q!Ap1hy_bNruHLH}wM0T&ed`M~!BryuPk_;dRkw*a82 diff --git a/src/test/scala/org/sunbird/cloud/storage/service/TestAzureStorageService.scala b/src/test/scala/org/sunbird/cloud/storage/service/TestAzureStorageService.scala deleted file mode 100644 index 4a72aaf..0000000 --- a/src/test/scala/org/sunbird/cloud/storage/service/TestAzureStorageService.scala +++ /dev/null @@ -1,72 +0,0 @@ -package org.sunbird.cloud.storage.service - -import org.sunbird.cloud.storage.factory.StorageConfig -import org.sunbird.cloud.storage.factory.StorageServiceFactory -import org.scalatest._ -import org.scalatest.flatspec.AnyFlatSpec -import org.scalatest.matchers.should.Matchers -import org.sunbird.cloud.storage.conf.AppConf -import org.sunbird.cloud.storage.exception.StorageServiceException - -class TestAzureStorageService extends AnyFlatSpec with Matchers { - - it should "test for azure storage" in { - - val azureService = StorageServiceFactory.getStorageService(StorageConfig("azure", AppConf.getStorageKey, AppConf.getStorageSecret)) - - val storageContainer = AppConf.getConfig("cloud_storage_container") - - azureService.upload(storageContainer, "src/test/resources/test-data.log", "testUpload/test-blob.log", Option(false), Option(1), Option(2), None) - // azureService.download(storageContainer, "testUpload/test-blob.log", "src/test/resources/test-azure/") - - // upload directory - println("url of folder", azureService.upload(storageContainer, "src/test/resources/1234/", "testUpload/1234/", Option(true), Option(1), Option(2), None)) - - // downlaod directory - azureService.download(storageContainer, "testUpload/1234/", "src/test/resources/test-azure/", Option(true)) - - println("azure signed url", azureService.getSignedURL(storageContainer, "testUpload/test-blob.log", Option(600))) - - val blob = azureService.getObject(storageContainer, "testUpload/test-blob.log") - println("blob details: ", blob) - - val folderUri = azureService.getUri(storageContainer, "testUpload", Option(true)) - println("Folder URI: ", folderUri) - val fileUri = azureService.getUri(storageContainer, "testUpload/test-blob.log", Option(false)) - println("File URI: ", fileUri) - -// println("upload public url", azureService.upload(storageContainer, "src/test/resources/test-data.log", "testUpload/test-data-public.log", Option(true))) -// println("upload public with expiry url", azureService.upload(storageContainer, "src/test/resources/test-data.log", "testUpload/test-data-with-expiry.log", Option(true), Option(false), Option(600))) -// println("signed path to upload from external client", azureService.getSignedURL(storageContainer, "testUpload/test-data-public1.log", Option(600), Option("w"))) - - val keys = azureService.searchObjectkeys(storageContainer, "testUpload/1234/") - keys.foreach(f => println(f)) - val blobs = azureService.searchObjects(storageContainer, "testUpload/1234/") - blobs.foreach(f => println(f)) - - val objData = azureService.getObjectData(storageContainer, "testUpload/test-blob.log") - objData.length should be(18) - - // delete directory -// azureService.deleteObject(storageContainer, "testUpload/1234/", Option(true)) - //azureService.deleteObject(storageContainer, "testUpload/test-blob.log") - //azureUtil.deleteObject(storageContainer, "testUpload/test-data-public.log") - //azureUtil.deleteObject(storageContainer, "testUpload/test-data-with-expiry.log") - //azureService.deleteObject(storageContainer, "testDuplicate/1234/", Option(true)) - - //azureService.upload(storageContainer, "src/test/resources/test-extract.zip", "testUpload/test-extract.zip") - //println(azureService.upload(storageContainer, "src/test/resources/110Mb_File.zip", "testUpload/110Mb_File.zip", Option(false),Option(false),None, Option(3), 1)) - //azureService.copyObjects(storageContainer, "testUpload/test-extract.zip", storageContainer, "testDuplicate/test-extract.zip") - //azureService.copyObjects(storageContainer, "testUpload/1234/", storageContainer, "testDuplicate/1234/", Option(true)) - - //azureService.extractArchive(storageContainer, "testUpload/test-extract.zip", "testUpload/test-extract/") - - val caught = - intercept[StorageServiceException]{ - azureService.upload(storageContainer, "src/test/resources/1234/test-blob.log", "testUpload/1234/", Option(false), Option(5),Option(2), None) - } - assert(caught.getMessage.contains("Failed to upload.")) - - azureService.closeContext() - } -} diff --git a/src/test/scala/org/sunbird/cloud/storage/service/TestGcloudStorageService.scala b/src/test/scala/org/sunbird/cloud/storage/service/TestGcloudStorageService.scala deleted file mode 100644 index a91c482..0000000 --- a/src/test/scala/org/sunbird/cloud/storage/service/TestGcloudStorageService.scala +++ /dev/null @@ -1,70 +0,0 @@ -package org.sunbird.cloud.storage.service - -import org.scalatest.flatspec.AnyFlatSpec -import org.scalatest.matchers.should.Matchers -import org.sunbird.cloud.storage.conf.AppConf -import org.sunbird.cloud.storage.exception.StorageServiceException -import org.sunbird.cloud.storage.factory.{StorageConfig, StorageServiceFactory} - -class TestGcloudStorageService extends AnyFlatSpec with Matchers { - - it should "test for gcloud storage" in { - - val gsService = StorageServiceFactory.getStorageService(StorageConfig("gcloud", AppConf.getStorageKey, AppConf.getStorageSecret)) - - val storageContainer = AppConf.getConfig("cloud_storage_container") - -// gsService.upload(storageContainer, "src/test/resources/test-data.log", "testUpload/test-blob.log", Option(false), Option(1), Option(2), None) -// gsService.upload(storageContainer, "src/test/resources/test-extract.zip", "testUpload/test-extract.zip", Option(false), Option(1), Option(2), None) -// -// // upload directory -// println("url of folder", gsService.upload(storageContainer, "src/test/resources/1234/", "testUpload/1234/", Option(true), Option(1), Option(2), None)) -// -// // downlaod directory -// gsService.download(storageContainer, "testUpload/1234/", "src/test/resources/test-gcloud/", Option(true)) -// -// println("gcloud signed url", gsService.getSignedURL(storageContainer, "testUpload/test-blob.log", Option(600))) -// -// val blob = gsService.getObject(storageContainer, "testUpload/test-blob.log") -// println("blob details: ", blob) -// -// val folderUri = gsService.getUri(storageContainer, "testUpload", Option(true)) -// println("Folder URI: ", folderUri) -// val fileUri = gsService.getUri(storageContainer, "testUpload/test-blob.log", Option(false)) -// println("File URI: ", fileUri) -// -// val keys = gsService.searchObjectkeys(storageContainer, "testUpload/1234/") -// keys.foreach(f => println(f)) -// val blobs = gsService.searchObjects(storageContainer, "testUpload/1234/") -// blobs.foreach(f => println(f)) -// -// val objData = gsService.getObjectData(storageContainer, "testUpload/test-blob.log") -// objData.length should be(18) - -// println("signed path to upload from external client", gsService.getPutSignedURL(storageContainer, "testUpload/test-data-public1.log", Option(600), Option("w"), Option("text/plain"))) - -// gsService.copyObjects(storageContainer, "testUpload/1234/", storageContainer, "testDuplicate/1234/", Option(true)) -// gsService.extractArchive(storageContainer, "testUpload/test-extract.zip", "testUpload/test-extract/") - - // delete directory -// gsService.deleteObject(storageContainer, "testUpload/1234/", Option(true)) -// gsService.deleteObject(storageContainer, "testUpload/test-blob.log") -// gsService.deleteObject(storageContainer, "testUpload/test-extract.zip") -// gsService.deleteObject(storageContainer, "testDuplicate/", Option(true)) -// gsService.deleteObject(storageContainer, "testUpload/test-extract/", Option(true)) - - val caught1 = - intercept[StorageServiceException]{ - gsService.getSignedURL(storageContainer, "testUpload/test-data-public1.log", Option(600), Option("w")) - } - assert(caught1.getMessage.contains("getSignedURL method is not supported for GCP. Please use getPutSignedURL with contentType.")) - - val caught2 = - intercept[StorageServiceException]{ - gsService.upload(storageContainer, "src/test/resources/1234/test-blob.log", "testUpload/1234/", Option(false), Option(5),Option(2), None) - } - assert(caught2.getMessage.contains("Failed to upload.")) - - gsService.closeContext() - } -} diff --git a/src/test/scala/org/sunbird/cloud/storage/service/TestGcloudStorageServiceGetPutSignedURL.scala b/src/test/scala/org/sunbird/cloud/storage/service/TestGcloudStorageServiceGetPutSignedURL.scala deleted file mode 100644 index c2767c7..0000000 --- a/src/test/scala/org/sunbird/cloud/storage/service/TestGcloudStorageServiceGetPutSignedURL.scala +++ /dev/null @@ -1,53 +0,0 @@ -package org.sunbird.cloud.storage.service - -import org.scalatest.flatspec.AnyFlatSpec -import org.scalatest.matchers.should.Matchers -import org.sunbird.cloud.storage.conf.AppConf -import org.sunbird.cloud.storage.factory.{StorageConfig, StorageServiceFactory} -import scala.io.Source -import org.sunbird.cloud.storage.util.JSONUtils - -class TestGcloudStorageServiceGetPutSignedURL extends AnyFlatSpec with Matchers { - - ignore should "return a valid V4 PUT signed URL for GCP" in { - - // 1) Compose service via factory - val gsService = StorageServiceFactory.getStorageService(StorageConfig("gcloud", AppConf.getStorageKey, AppConf.getStorageSecret)) - - val storageContainer = AppConf.getConfig("gcloud_storage_container") - // val objectKey = s"testUpload/test-put-signed-${System.currentTimeMillis()}.log" - val objectKey = s"testUpload/lesffichage.mp4" - - // 2) Build additionalParams from gcs-creds.json using JSONUtils - val credsPath = "src/test/resources/gcs-creds.json" - val credsJson = Source.fromFile(credsPath).getLines().mkString - val creds: Map[String, Any] = JSONUtils.deserialize[Map[String, Any]](credsJson) - - def str(key: String): String = creds.get(key).map(_.toString).getOrElse("") - - val privateKeyFixed = str("private_key").replace("\\n", "\n") - - val additionalParams: Map[String, String] = Map( - "clientId" -> str("client_id"), - "clientEmail" -> str("client_email"), - "privateKeyPkcs8" -> privateKeyFixed, - "privateKeyIds" -> str("private_key_id"), - "projectId" -> str("project_id"), - "chunked" -> "true" - ) - - // Call getPutSignedURL with write permission and a contentType - val ttlSeconds = Option(86400) - val contentType = Option("video/mp4") - val url = gsService.getPutSignedURL(storageContainer, objectKey, ttlSeconds, Option("w"), contentType, Option(additionalParams)) - println("Put Signed URL: " + url) - - // Basic validity assertions - url should include ("https://storage.googleapis.com/") - url should include (storageContainer) - url should include (objectKey) - url.toLowerCase should include ("x-goog-signature") - - gsService.closeContext() - } -} diff --git a/src/test/scala/org/sunbird/cloud/storage/service/TestOCIS3StorageService.scala b/src/test/scala/org/sunbird/cloud/storage/service/TestOCIS3StorageService.scala deleted file mode 100644 index 92581c1..0000000 --- a/src/test/scala/org/sunbird/cloud/storage/service/TestOCIS3StorageService.scala +++ /dev/null @@ -1,68 +0,0 @@ -package org.sunbird.cloud.storage.service - -import org.scalatest.flatspec.AnyFlatSpec -import org.scalatest.matchers.should.Matchers -import org.sunbird.cloud.storage.conf.AppConf -import org.sunbird.cloud.storage.exception.StorageServiceException -import org.sunbird.cloud.storage.factory.{StorageConfig, StorageServiceFactory} - -class TestOCIS3StorageService extends AnyFlatSpec with Matchers { - - it should "test for OCIS3 storage" in { - - val storageConfig = StorageConfig("oci", AppConf.getStorageKey, AppConf.getStorageSecret, Option("https://xyz.compat.objectstorage.ap-hyderabad-1.oraclecloud.com"), AppConf.getRegion) - val ociS3Service = StorageServiceFactory.getStorageService(storageConfig) - - val storageContainer = AppConf.getConfig("cloud_storage_container") - - // Use this exception block to execute the test cases successfully when it has invalid configuration. - val caught = - intercept[StorageServiceException]{ - ociS3Service.upload(storageContainer, "src/test/resources/1234/test-blob.log", "testUpload/1234/", Option(false),Option(5), Option(2), None) - } - assert(caught.getMessage.contains("Failed to upload.")) - - /** - * Use the below complete block when we have the valid configuration and - * to test the OCI functionality. - */ - /** - ociS3Service.upload(storageContainer, "src/test/resources/test-data.log", "testUpload/test-blob.log") - ociS3Service.download(storageContainer, "testUpload/test-blob.log", "src/test/resources/test-s3/") - - // upload directory - println("url of folder", ociS3Service.upload(storageContainer, "src/test/resources/1234/", "testUpload/1234/", Option(true))) - - // downlaod directory - ociS3Service.download(storageContainer, "testUpload/1234/", "src/test/resources/test-s3/", Option(true)) - - println("OCI S3 signed url", ociS3Service.getSignedURL(storageContainer, "testUpload/test-blob.log", Option(600))) - - val blob = ociS3Service.getObject(storageContainer, "testUpload/test-blob.log") - println("blob details: ", blob) - - println("upload public url", ociS3Service.upload(storageContainer, "src/test/resources/test-data.log", "testUpload/test-data-public.log", Option(true))) - println("upload public with expiry url", ociS3Service.upload(storageContainer, "src/test/resources/test-data.log", "testUpload/test-data-with-expiry.log", Option(false))) - println("signed path to upload from external client", ociS3Service.getSignedURL(storageContainer, "testUpload/test-data-public1.log", Option(600), Option("w"))) - - val keys = ociS3Service.searchObjectkeys(storageContainer, "testUpload/1234/") - keys.foreach(f => println(f)) - val blobs = ociS3Service.searchObjects(storageContainer, "testUpload/1234/") - blobs.foreach(f => println(f)) - - val objData = ociS3Service.getObjectData(storageContainer, "testUpload/test-blob.log") - objData.length should be(18) - - // delete directory - ociS3Service.deleteObject(storageContainer, "testUpload/1234/", Option(true)) - ociS3Service.deleteObject(storageContainer, "testUpload/test-blob.log") - - ociS3Service.upload(storageContainer, "src/test/resources/test-extract.zip", "testUpload/test-extract.zip") - ociS3Service.copyObjects(storageContainer, "testUpload/test-extract.zip", storageContainer, "testDuplicate/test-extract.zip") - - ociS3Service.extractArchive(storageContainer, "testUpload/test-extract.zip", "testUpload/test-extract/") - */ - ociS3Service.closeContext() - } - -} diff --git a/src/test/scala/org/sunbird/cloud/storage/service/TestS3IAMStorageService.scala b/src/test/scala/org/sunbird/cloud/storage/service/TestS3IAMStorageService.scala deleted file mode 100644 index d794a40..0000000 --- a/src/test/scala/org/sunbird/cloud/storage/service/TestS3IAMStorageService.scala +++ /dev/null @@ -1,62 +0,0 @@ -package org.sunbird.cloud.storage.service - -import org.scalatest.flatspec.AnyFlatSpec -import org.scalatest.matchers.should.Matchers -import org.sunbird.cloud.storage.factory.{StorageConfig, StorageServiceFactory} - -/** - * Test S3 storage service with IAM role authentication. - * Run this test on an EC2 instance with IAM role attached. - * - * Prerequisites: - * 1. EC2 instance with IAM role that has S3 read permissions - * 2. Update 'bucket' variable with your actual S3 bucket name - * 3. Update 'region' in StorageConfig with your bucket's region - * - * Run: mvn test -Dtest=TestS3IAMStorageService - */ -class TestS3IAMStorageService extends AnyFlatSpec with Matchers { - - // TODO: Replace with your actual bucket name and region before running on EC2 - val bucket = "your-test-bucket" - val region = "us-east-1" - - ignore should "test S3 storage with IAM role authentication" in { - println("=== Starting IAM Role Authentication Test ===\n") - - // Create service with IAM role auth (no access keys needed) - val s3Service = StorageServiceFactory.getStorageService( - StorageConfig("aws", "", "", None, Option(region), Option("iam_role")) - ) - - // Test 1: List objects in bucket - println("=== Test 1: listObjectKeys ===") - val keys = s3Service.listObjectKeys(bucket, "") - println(s"Found ${keys.size} objects in bucket '$bucket'") - if (keys.nonEmpty) { - println("First 5 objects:") - keys.take(5).foreach(k => println(s" - $k")) - } - - // Test 2: Get object metadata (if objects exist) - if (keys.nonEmpty) { - println("\n=== Test 2: getObject ===") - val blob = s3Service.getObject(bucket, keys.head) - println(s"Object key: ${blob.key}") - println(s"Content length: ${blob.contentLength} bytes") - println(s"Last modified: ${blob.lastModified}") - } else { - println("\n=== Test 2: getObject (skipped - no objects in bucket) ===") - } - - // Test 3: Search objects with prefix - println("\n=== Test 3: searchObjectkeys ===") - val searchResults = s3Service.searchObjectkeys(bucket, "") - println(s"Search returned ${searchResults.size} objects") - - // Cleanup - s3Service.closeContext() - - println("\n=== IAM Role Authentication Test COMPLETED ===") - } -} diff --git a/src/test/scala/org/sunbird/cloud/storage/service/TestS3StorageService.scala b/src/test/scala/org/sunbird/cloud/storage/service/TestS3StorageService.scala deleted file mode 100644 index c8b1564..0000000 --- a/src/test/scala/org/sunbird/cloud/storage/service/TestS3StorageService.scala +++ /dev/null @@ -1,66 +0,0 @@ -package org.sunbird.cloud.storage.service - -import org.scalatest.flatspec.AnyFlatSpec -import org.scalatest.matchers.should.Matchers -import org.sunbird.cloud.storage.conf.AppConf -import org.sunbird.cloud.storage.exception.StorageServiceException -import org.sunbird.cloud.storage.factory.{StorageConfig, StorageServiceFactory} - -class TestS3StorageService extends AnyFlatSpec with Matchers { - - it should "test for s3 storage" in { - - val s3Service = StorageServiceFactory.getStorageService(StorageConfig("aws", AppConf.getStorageKey, AppConf.getStorageSecret)) - - val storageContainer = AppConf.getConfig("cloud_storage_container") - - //println(s3Service.upload(storageContainer, "src/test/resources/110Mb_File.zip", "testUpload/110Mb_File.zip", Option(false),Option(false),None, Option(3), 1)) - val caught = - intercept[StorageServiceException]{ - s3Service.upload(storageContainer, "src/test/resources/1234/test-blob.log", "testUpload/1234/", Option(false),Option(5), Option(2), None) - } - assert(caught.getMessage.contains("Failed to upload.")) - /* - s3Service.upload(storageContainer, "src/test/resources/test-data.log", "testUpload/test-blob.log") - s3Service.download(storageContainer, "testUpload/test-blob.log", "src/test/resources/test-s3/") - // upload directory - println("url of folder", s3Service.upload(storageContainer, "src/test/resources/1234/", "testUpload/1234/", None, Option(true))) - // downlaod directory - s3Service.download(storageContainer, "testUpload/1234/", "src/test/resources/test-s3/", Option(true)) - println("aws signed url", s3Service.getSignedURL(storageContainer, "testUpload/test-blob.log", Option(600))) - val blob = s3Service.getObject(storageContainer, "testUpload/test-blob.log") - println("blob details: ", blob) - println("upload public url", s3Service.upload(storageContainer, "src/test/resources/test-data.log", "testUpload/test-data-public.log", Option(true))) - println("upload public with expiry url", s3Service.upload(storageContainer, "src/test/resources/test-data.log", "testUpload/test-data-with-expiry.log", Option(true), Option(false), Option(600))) - println("signed path to upload from external client", s3Service.getSignedURL(storageContainer, "testUpload/test-data-public1.log", Option(600), Option("w"))) - val keys = s3Service.searchObjectkeys(storageContainer, "testUpload/1234/") - keys.foreach(f => println(f)) - val blobs = s3Service.searchObjects(storageContainer, "testUpload/1234/") - blobs.foreach(f => println(f)) - val objData = s3Service.getObjectData(storageContainer, "testUpload/test-blob.log") - objData.length should be(18) - // delete directory - s3Service.deleteObject(storageContainer, "testUpload/1234/", Option(true)) - s3Service.deleteObject(storageContainer, "testUpload/test-blob.log") - //s3Service.deleteObject(storageContainer, "testUpload/test-data-public.log") - //s3Service.deleteObject(storageContainer, "testUpload/test-data-with-expiry.log") - s3Service.upload(storageContainer, "src/test/resources/test-extract.zip", "testUpload/test-extract.zip") - s3Service.copyObjects(storageContainer, "testUpload/test-extract.zip", storageContainer, "testDuplicate/test-extract.zip") - s3Service.extractArchive(storageContainer, "testUpload/test-extract.zip", "testUpload/test-extract/") - */ - - // Test if the listObjectKeys lists all objects inside a directory when the number of objects count - // is greater than threshold value 1000 - /* val objectsListInsideADirectory = s3Service.listObjectKeys("ekstep-public-dev", _prefix = "content/h5p/do_112620111166709760183-snapshot/content") - objectsListInsideADirectory.size should be (1200) - // Test if the listObjectKeys lists individual files if the prefix is not a directory - val fileObject = s3Service.listObjectKeys("ekstep-public-dev", _prefix = "content/h5p/do_112620111166709760183-snapshot/index.html") - fileObject.size should be (1) - fileObject.head should be ("content/h5p/do_112620111166709760183-snapshot/index.html") - // Test if the listObjectKeys lists no files if the filename is not found - val noFilesFound = s3Service.listObjectKeys("ekstep-public-dev", _prefix = "random_file_name_which_is_not_present") - noFilesFound.size should be (0)*/ - - s3Service.closeContext() - } -} \ No newline at end of file From edab16b80841eecaa4aa3ecf8882791f516b819f Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 20 Feb 2026 05:10:17 +0000 Subject: [PATCH 27/47] test: Add unit and integration tests for all modules - API module: unit tests for DateRangeUtil, FileUtil, and AbstractStorageService (in-memory implementation, no credentials) - CSP modules: integration tests extending BaseStorageServiceIntegrationTest that auto-skip via JUnit 5 assumeTrue() when env vars are absent - API POM: publish test-jar so CSP modules can extend the base test class - CSP POMs: depend on api test-jar for shared test infrastructure - TESTING.md: comprehensive guide covering env vars, auth types, and examples for running tests from cloud-permissioned VMs https://claude.ai/code/session_01KrRXhCQDoUCac99315Fnh6 --- TESTING.md | 285 ++++++++++++ cloud-storage-sdk-api/pom.xml | 18 + .../storage/AbstractStorageServiceTest.java | 422 ++++++++++++++++++ .../BaseStorageServiceIntegrationTest.java | 289 ++++++++++++ .../cloud/storage/util/DateRangeUtilTest.java | 69 +++ .../cloud/storage/util/FileUtilTest.java | 124 +++++ cloud-storage-sdk-aws/pom.xml | 6 + .../service/aws/AwsStorageServiceTest.java | 89 ++++ cloud-storage-sdk-azure/pom.xml | 6 + .../azure/AzureStorageServiceTest.java | 78 ++++ cloud-storage-sdk-gcp/pom.xml | 6 + .../service/gcp/GcpStorageServiceTest.java | 78 ++++ cloud-storage-sdk-oci/pom.xml | 6 + .../service/oci/OciStorageServiceTest.java | 89 ++++ 14 files changed, 1565 insertions(+) create mode 100644 TESTING.md create mode 100644 cloud-storage-sdk-api/src/test/java/org/sunbird/cloud/storage/AbstractStorageServiceTest.java create mode 100644 cloud-storage-sdk-api/src/test/java/org/sunbird/cloud/storage/BaseStorageServiceIntegrationTest.java create mode 100644 cloud-storage-sdk-api/src/test/java/org/sunbird/cloud/storage/util/DateRangeUtilTest.java create mode 100644 cloud-storage-sdk-api/src/test/java/org/sunbird/cloud/storage/util/FileUtilTest.java create mode 100644 cloud-storage-sdk-aws/src/test/java/org/sunbird/cloud/storage/service/aws/AwsStorageServiceTest.java create mode 100644 cloud-storage-sdk-azure/src/test/java/org/sunbird/cloud/storage/service/azure/AzureStorageServiceTest.java create mode 100644 cloud-storage-sdk-gcp/src/test/java/org/sunbird/cloud/storage/service/gcp/GcpStorageServiceTest.java create mode 100644 cloud-storage-sdk-oci/src/test/java/org/sunbird/cloud/storage/service/oci/OciStorageServiceTest.java diff --git a/TESTING.md b/TESTING.md new file mode 100644 index 0000000..3daf178 --- /dev/null +++ b/TESTING.md @@ -0,0 +1,285 @@ +# Testing Guide + +This document explains how to run tests for the Cloud Storage SDK, including +unit tests that run without any cloud credentials and integration tests that +require access to real cloud storage. + +--- + +## Unit Tests (no credentials required) + +The **cloud-storage-sdk-api** module contains pure unit tests that validate +shared logic (date utilities, file utilities, abstract orchestration) without +connecting to any cloud provider. They run with an in-memory storage +implementation. + +```bash +# Run API unit tests only +mvn -pl cloud-storage-sdk-api test + +# Run all unit tests across all modules (integration tests auto-skip when +# environment variables are absent) +mvn test +``` + +The unit tests always execute; no special configuration is needed. + +--- + +## Integration Tests + +Each CSP module contains an integration test class that exercises the full +storage operations (upload, download, signed URLs, copy, delete, search, etc.) +against a real cloud bucket. + +**Integration tests are skipped by default.** They only execute when specific +environment variables are set, making it safe to run `mvn test` in any +environment — CI pipelines, local laptops, or cloud VMs. + +### How the skip mechanism works + +Each CSP test uses JUnit 5 `assumeTrue()` guards. When the required +`*_STORAGE_TEST_ENABLED` environment variable is absent, JUnit marks +the tests as **skipped** (not failed), so CI pipelines remain green. + +--- + +### AWS S3 + +**Test class:** `cloud-storage-sdk-aws` → `AwsStorageServiceTest` + +| Variable | Required | Description | +|---|---|---| +| `AWS_STORAGE_TEST_ENABLED` | Yes | Any non-empty value (e.g. `true`) enables the tests | +| `AWS_STORAGE_TEST_CONTAINER` | Yes | Name of a pre-existing, writable S3 bucket | +| `AWS_STORAGE_TEST_AUTH_TYPE` | No | `ACCESS_KEY` (default), `IAM`, `IAM_ROLE`, or `INSTANCE_PROFILE` | +| `AWS_ACCESS_KEY_ID` | If ACCESS_KEY | Standard AWS access key | +| `AWS_SECRET_ACCESS_KEY` | If ACCESS_KEY | Standard AWS secret key | +| `AWS_REGION` | No | AWS region (default: `us-east-1`) | +| `AWS_STORAGE_TEST_ENDPOINT` | No | Custom endpoint for Ceph S3-compatible storage | +| `AWS_STORAGE_TEST_TYPE` | No | `AWS` (default) or `CEPHS3` | + +#### Example: EC2 instance with IAM role + +On an EC2 instance (or EKS pod) that has an IAM role with S3 permissions: + +```bash +export AWS_STORAGE_TEST_ENABLED=true +export AWS_STORAGE_TEST_CONTAINER=my-test-bucket +export AWS_STORAGE_TEST_AUTH_TYPE=INSTANCE_PROFILE +export AWS_REGION=us-east-1 + +mvn -pl cloud-storage-sdk-aws test +``` + +#### Example: Local development with access keys + +```bash +export AWS_STORAGE_TEST_ENABLED=true +export AWS_STORAGE_TEST_CONTAINER=my-test-bucket +export AWS_ACCESS_KEY_ID=AKIA... +export AWS_SECRET_ACCESS_KEY=wJal... +export AWS_REGION=us-east-1 + +mvn -pl cloud-storage-sdk-aws test +``` + +#### Example: Ceph S3-compatible storage + +```bash +export AWS_STORAGE_TEST_ENABLED=true +export AWS_STORAGE_TEST_CONTAINER=my-ceph-bucket +export AWS_STORAGE_TEST_TYPE=CEPHS3 +export AWS_STORAGE_TEST_ENDPOINT=https://ceph-rgw.example.com +export AWS_ACCESS_KEY_ID=ceph-key +export AWS_SECRET_ACCESS_KEY=ceph-secret +export AWS_REGION=us-east-1 + +mvn -pl cloud-storage-sdk-aws test +``` + +--- + +### Azure Blob Storage + +**Test class:** `cloud-storage-sdk-azure` → `AzureStorageServiceTest` + +| Variable | Required | Description | +|---|---|---| +| `AZURE_STORAGE_TEST_ENABLED` | Yes | Any non-empty value enables the tests | +| `AZURE_STORAGE_TEST_CONTAINER` | Yes | Name of a pre-existing, writable blob container | +| `AZURE_STORAGE_ACCOUNT_NAME` | Yes | Azure Storage account name | +| `AZURE_STORAGE_TEST_AUTH_TYPE` | No | `ACCESS_KEY` (default), `OIDC`, `IAM`, or `INSTANCE_PROFILE` | +| `AZURE_STORAGE_ACCOUNT_KEY` | If ACCESS_KEY | Storage account key | +| `AZURE_CLIENT_ID` | If OIDC | Service principal / managed identity client ID | +| `AZURE_TENANT_ID` | If OIDC | Azure AD tenant ID | +| `AZURE_FEDERATED_TOKEN_FILE` | If OIDC | Path to the federated token file (Kubernetes workload identity) | + +#### Example: Azure VM with Managed Identity + +On an Azure VM or AKS pod with a system-assigned or user-assigned managed +identity that has `Storage Blob Data Contributor` role: + +```bash +export AZURE_STORAGE_TEST_ENABLED=true +export AZURE_STORAGE_TEST_CONTAINER=my-test-container +export AZURE_STORAGE_ACCOUNT_NAME=mystorageaccount +export AZURE_STORAGE_TEST_AUTH_TYPE=IAM + +mvn -pl cloud-storage-sdk-azure test +``` + +#### Example: Local development with account key + +```bash +export AZURE_STORAGE_TEST_ENABLED=true +export AZURE_STORAGE_TEST_CONTAINER=my-test-container +export AZURE_STORAGE_ACCOUNT_NAME=mystorageaccount +export AZURE_STORAGE_ACCOUNT_KEY=base64encodedkey... + +mvn -pl cloud-storage-sdk-azure test +``` + +--- + +### Google Cloud Storage + +**Test class:** `cloud-storage-sdk-gcp` → `GcpStorageServiceTest` + +| Variable | Required | Description | +|---|---|---| +| `GCP_STORAGE_TEST_ENABLED` | Yes | Any non-empty value enables the tests | +| `GCP_STORAGE_TEST_CONTAINER` | Yes | Name of a pre-existing, writable GCS bucket | +| `GCP_STORAGE_TEST_AUTH_TYPE` | No | `ACCESS_KEY` (default), `OIDC`, or `IAM` | +| `GCP_SERVICE_ACCOUNT_KEY_JSON` | If ACCESS_KEY | Full JSON content of a service account key | +| `GCP_STORAGE_TEST_PROJECT` | No | GCP project ID | +| `GOOGLE_APPLICATION_CREDENTIALS` | If OIDC/IAM | Standard GCP env var pointing to a credentials file | + +#### Example: GCE VM with default service account + +On a Compute Engine instance (or GKE pod) with Workload Identity or the +default compute service account that has `roles/storage.objectAdmin`: + +```bash +export GCP_STORAGE_TEST_ENABLED=true +export GCP_STORAGE_TEST_CONTAINER=my-test-bucket +export GCP_STORAGE_TEST_AUTH_TYPE=IAM + +mvn -pl cloud-storage-sdk-gcp test +``` + +#### Example: Local development with service account key + +```bash +export GCP_STORAGE_TEST_ENABLED=true +export GCP_STORAGE_TEST_CONTAINER=my-test-bucket +export GCP_SERVICE_ACCOUNT_KEY_JSON='{"type":"service_account","project_id":"...","private_key":"..."}' + +mvn -pl cloud-storage-sdk-gcp test +``` + +--- + +### Oracle Cloud Infrastructure Object Storage + +**Test class:** `cloud-storage-sdk-oci` → `OciStorageServiceTest` + +| Variable | Required | Description | +|---|---|---| +| `OCI_STORAGE_TEST_ENABLED` | Yes | Any non-empty value enables the tests | +| `OCI_STORAGE_TEST_CONTAINER` | Yes | Name of a pre-existing, writable OCI bucket | +| `OCI_STORAGE_TEST_AUTH_TYPE` | No | `ACCESS_KEY` (default), `INSTANCE_PROFILE`, or `IAM` | +| `OCI_USER_ID` | If ACCESS_KEY | User OCID (mapped to `storageKey`) | +| `OCI_PRIVATE_KEY_PEM` | If ACCESS_KEY | PEM content of the API signing key (mapped to `storageSecret`) | +| `OCI_TENANCY_ID` | If ACCESS_KEY | Tenancy OCID | +| `OCI_FINGERPRINT` | If ACCESS_KEY | Key fingerprint | +| `OCI_REGION` | No | OCI region (default: `us-ashburn-1`) | + +#### Example: OCI Compute instance with Instance Principal + +On an OCI Compute instance or OKE pod with an Instance Principal dynamic group +policy granting `manage objects` and `manage buckets`: + +```bash +export OCI_STORAGE_TEST_ENABLED=true +export OCI_STORAGE_TEST_CONTAINER=my-test-bucket +export OCI_STORAGE_TEST_AUTH_TYPE=INSTANCE_PROFILE +export OCI_REGION=us-ashburn-1 + +mvn -pl cloud-storage-sdk-oci test +``` + +#### Example: Local development with API key + +```bash +export OCI_STORAGE_TEST_ENABLED=true +export OCI_STORAGE_TEST_CONTAINER=my-test-bucket +export OCI_USER_ID=ocid1.user.oc1..aaaa... +export OCI_TENANCY_ID=ocid1.tenancy.oc1..aaaa... +export OCI_FINGERPRINT=aa:bb:cc:dd:ee:ff:00:11:22:33:44:55:66:77:88:99 +export OCI_PRIVATE_KEY_PEM="$(cat ~/.oci/oci_api_key.pem)" +export OCI_REGION=us-ashburn-1 + +mvn -pl cloud-storage-sdk-oci test +``` + +--- + +## Running All Integration Tests + +To run integration tests for multiple (or all) providers at once, set the +relevant environment variables and run: + +```bash +mvn test +``` + +Only the CSPs whose `*_STORAGE_TEST_ENABLED` variable is set will actually +execute; all others are skipped. + +## Running a Single CSP Module + +```bash +# AWS only +mvn -pl cloud-storage-sdk-aws test + +# Azure only +mvn -pl cloud-storage-sdk-azure test + +# GCP only +mvn -pl cloud-storage-sdk-gcp test + +# OCI only +mvn -pl cloud-storage-sdk-oci test +``` + +## Running a Specific Test Method + +```bash +mvn -pl cloud-storage-sdk-aws test \ + -Dtest="AwsStorageServiceTest#testUploadAndDownload" +``` + +--- + +## CI/CD Notes + +- **Default behaviour:** `mvn test` runs unit tests and skips all integration + tests (since `*_STORAGE_TEST_ENABLED` is not set). This is safe for all CI + environments. +- **Integration gate:** To add an integration test gate in CI, set the required + environment variables as secrets in your CI system and enable the specific CSP + tests in a dedicated pipeline stage. +- **Cleanup:** Integration tests create objects under a unique UUID-prefixed + path and delete them in `@AfterAll`. If a test run is interrupted, you may + need to manually clean up objects matching the `test-/` prefix from the + test bucket. + +## Prerequisites + +| Provider | Minimum permissions | +|---|---| +| AWS | `s3:PutObject`, `s3:GetObject`, `s3:DeleteObject`, `s3:ListBucket` on the test bucket | +| Azure | `Storage Blob Data Contributor` role on the test container | +| GCP | `roles/storage.objectAdmin` on the test bucket | +| OCI | `manage objects` and `read buckets` in the test compartment | diff --git a/cloud-storage-sdk-api/pom.xml b/cloud-storage-sdk-api/pom.xml index 787e4fe..546dc3b 100644 --- a/cloud-storage-sdk-api/pom.xml +++ b/cloud-storage-sdk-api/pom.xml @@ -33,4 +33,22 @@ test
+ + + + + + org.apache.maven.plugins + maven-jar-plugin + + + test-jar + + test-jar + + + + + + diff --git a/cloud-storage-sdk-api/src/test/java/org/sunbird/cloud/storage/AbstractStorageServiceTest.java b/cloud-storage-sdk-api/src/test/java/org/sunbird/cloud/storage/AbstractStorageServiceTest.java new file mode 100644 index 0000000..0b95a29 --- /dev/null +++ b/cloud-storage-sdk-api/src/test/java/org/sunbird/cloud/storage/AbstractStorageServiceTest.java @@ -0,0 +1,422 @@ +package org.sunbird.cloud.storage; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; +import org.sunbird.cloud.storage.exception.StorageServiceException; +import org.sunbird.cloud.storage.model.Blob; +import org.sunbird.cloud.storage.model.DeleteTarget; + +import java.io.*; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.*; +import java.util.stream.Collectors; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * Unit tests for AbstractStorageService orchestration logic using an in-memory + * storage implementation — no cloud credentials required. + */ +class AbstractStorageServiceTest { + + @TempDir + Path tempDir; + + private InMemoryStorageService service; + + @BeforeEach + void setUp() { + service = new InMemoryStorageService(); + } + + // ── Upload / Put ────────────────────────────────────────────────────────── + + @Test + void upload_singleFile_returnsUri() throws IOException { + Path file = tempDir.resolve("file.txt"); + Files.writeString(file, "hello"); + + String uri = service.upload("bucket", file.toString(), "prefix/file.txt"); + + assertEquals("mem://bucket/prefix/file.txt", uri); + assertTrue(service.exists("bucket", "prefix/file.txt")); + } + + @Test + void upload_directory_uploadsAllFiles() throws IOException { + Path dir = tempDir.resolve("data"); + Files.createDirectory(dir); + Files.writeString(dir.resolve("a.txt"), "aaa"); + Files.writeString(dir.resolve("b.txt"), "bbb"); + + String result = service.upload("bucket", dir.toString(), "prefix/data", true, 1, 0, null); + + assertTrue(result.contains("prefix/data/a.txt")); + assertTrue(result.contains("prefix/data/b.txt")); + assertTrue(service.exists("bucket", "prefix/data/a.txt")); + assertTrue(service.exists("bucket", "prefix/data/b.txt")); + } + + @Test + void upload_exceedsMaxRetries_throwsException() { + // Non-existent file forces an exception, and retry count 1 means fail immediately + assertThrows(StorageServiceException.class, () -> + service.upload("bucket", "/no/such/file.txt", "key", false, 1, 1, null)); + } + + @Test + void put_bytesContent_returnsUri() { + byte[] content = "data bytes".getBytes(StandardCharsets.UTF_8); + String uri = service.put("bucket", content, "obj/data.bin"); + + assertEquals("mem://bucket/obj/data.bin", uri); + assertTrue(service.exists("bucket", "obj/data.bin")); + } + + // ── List / Get ──────────────────────────────────────────────────────────── + + @Test + void listObjectKeys_returnsMatchingKeys() { + service.putDirect("bucket", "logs/2024-01-01.txt", "a".getBytes()); + service.putDirect("bucket", "logs/2024-01-02.txt", "b".getBytes()); + service.putDirect("bucket", "other/file.txt", "c".getBytes()); + + List keys = service.listObjectKeys("bucket", "logs/"); + + assertEquals(2, keys.size()); + assertTrue(keys.contains("logs/2024-01-01.txt")); + assertTrue(keys.contains("logs/2024-01-02.txt")); + } + + @Test + void listObjects_returnsBlobs() { + service.putDirect("bucket", "data/file.txt", "content".getBytes()); + + List blobs = service.listObjects("bucket", "data/"); + + assertEquals(1, blobs.size()); + assertEquals("data/file.txt", blobs.get(0).getKey()); + } + + @Test + void getObject_returnsMetadata() { + service.putDirect("bucket", "key.txt", "hello".getBytes()); + + Blob blob = service.getObject("bucket", "key.txt"); + + assertEquals("key.txt", blob.getKey()); + assertEquals(5L, blob.getContentLength()); + } + + @Test + void getObject_withPayload_returnsBytes() { + service.putDirect("bucket", "data.txt", "payload".getBytes(StandardCharsets.UTF_8)); + + Blob blob = service.getObject("bucket", "data.txt", true); + + assertNotNull(blob.getPayload()); + assertEquals("payload", new String(blob.getPayload(), StandardCharsets.UTF_8)); + } + + @Test + void getObjectData_returnsLines() { + service.putDirect("bucket", "lines.txt", "line1\nline2\nline3".getBytes()); + + List lines = service.getObjectData("bucket", "lines.txt"); + + assertEquals(3, lines.size()); + assertEquals("line1", lines.get(0)); + } + + // ── Delete ──────────────────────────────────────────────────────────────── + + @Test + void deleteObject_removesFile() { + service.putDirect("bucket", "delete-me.txt", "x".getBytes()); + assertTrue(service.exists("bucket", "delete-me.txt")); + + service.deleteObject("bucket", "delete-me.txt"); + + assertFalse(service.exists("bucket", "delete-me.txt")); + } + + @Test + void deleteObjects_directory_removesAllKeysWithPrefix() { + service.putDirect("bucket", "dir/a.txt", "a".getBytes()); + service.putDirect("bucket", "dir/b.txt", "b".getBytes()); + service.putDirect("bucket", "other.txt", "o".getBytes()); + + service.deleteObjects("bucket", List.of(DeleteTarget.directory("dir/"))); + + assertFalse(service.exists("bucket", "dir/a.txt")); + assertFalse(service.exists("bucket", "dir/b.txt")); + assertTrue(service.exists("bucket", "other.txt")); + } + + @Test + void deleteObjects_multipleTargets() { + service.putDirect("bucket", "a.txt", "a".getBytes()); + service.putDirect("bucket", "b.txt", "b".getBytes()); + + service.deleteObjects("bucket", List.of( + DeleteTarget.file("a.txt"), + DeleteTarget.file("b.txt"))); + + assertFalse(service.exists("bucket", "a.txt")); + assertFalse(service.exists("bucket", "b.txt")); + } + + // ── Copy ───────────────────────────────────────────────────────────────── + + @Test + void copyObjects_singleFile_copiesCorrectly() { + service.putDirect("src-bucket", "original.txt", "data".getBytes()); + + service.copyObjects("src-bucket", "original.txt", "dst-bucket", "copy.txt"); + + assertTrue(service.exists("dst-bucket", "copy.txt")); + assertTrue(service.exists("src-bucket", "original.txt")); // original still there + } + + @Test + void copyObjects_directory_copiesAllFiles() { + service.putDirect("src", "folder/x.txt", "x".getBytes()); + service.putDirect("src", "folder/y.txt", "y".getBytes()); + + service.copyObjects("src", "folder/", "dst", "backup/", true); + + assertTrue(service.exists("dst", "backup/x.txt")); + assertTrue(service.exists("dst", "backup/y.txt")); + } + + // ── Search ──────────────────────────────────────────────────────────────── + + @Test + void searchObjects_withDateRange_returnsMatchingBlobs() { + service.putDirect("bucket", "logs/2024-03-01/data.txt", "a".getBytes()); + service.putDirect("bucket", "logs/2024-03-02/data.txt", "b".getBytes()); + service.putDirect("bucket", "logs/2024-03-03/data.txt", "c".getBytes()); + service.putDirect("bucket", "logs/2024-04-01/data.txt", "d".getBytes()); + + List results = service.searchObjects( + "bucket", "logs/", "2024-03-01", "2024-03-02", null, "yyyy-MM-dd"); + + assertEquals(2, results.size()); + } + + @Test + void searchObjectKeys_withDelta_returnsKeys() { + service.putDirect("bucket", "events/2024-05-08/file.txt", "x".getBytes()); + service.putDirect("bucket", "events/2024-05-09/file.txt", "y".getBytes()); + service.putDirect("bucket", "events/2024-05-10/file.txt", "z".getBytes()); + + List keys = service.searchObjectKeys( + "bucket", "events/", null, "2024-05-10", 2, "yyyy-MM-dd"); + + // delta=2 from 2024-05-10 means from 2024-05-08 to 2024-05-10 (inclusive) + assertEquals(3, keys.size()); + } + + @Test + void searchObjects_noDatesOrDelta_listsAllWithPrefix() { + service.putDirect("bucket", "prefix/a.txt", "a".getBytes()); + service.putDirect("bucket", "prefix/b.txt", "b".getBytes()); + service.putDirect("bucket", "other/c.txt", "c".getBytes()); + + List results = service.searchObjects( + "bucket", "prefix/", null, null, null, "yyyy-MM-dd"); + + assertEquals(2, results.size()); + } + + // ── URI / Paths ─────────────────────────────────────────────────────────── + + @Test + void getPaths_returnsHdfsPrefixedPaths() { + Blob b1 = new Blob("key1.txt", 10, new Date(), Map.of()); + Blob b2 = new Blob("key2.txt", 20, new Date(), Map.of()); + + List paths = service.getPaths("bucket", List.of(b1, b2)); + + assertEquals(List.of("mem://bucket/key1.txt", "mem://bucket/key2.txt"), paths); + } + + @Test + void getUri_returnsBaseUri() { + service.putDirect("bucket", "prefix/file.txt", "x".getBytes()); + + String uri = service.getUri("bucket", "prefix/"); + + assertEquals("mem://bucket/prefix/", uri); + } + + @Test + void getUri_emptyPrefix_throwsException() { + assertThrows(StorageServiceException.class, () -> + service.getUri("bucket", "nonexistent/prefix/")); + } + + // ── Download ────────────────────────────────────────────────────────────── + + @Test + void download_singleFile_writesToLocalPath() throws IOException { + service.putDirect("bucket", "remote/data.txt", "file-content".getBytes()); + String localDir = tempDir.toString() + "/"; + + service.download("bucket", "remote/data.txt", localDir); + + Path downloaded = tempDir.resolve("data.txt"); + assertTrue(Files.exists(downloaded)); + assertEquals("file-content", Files.readString(downloaded)); + } + + @Test + void download_directory_downloadsAllFiles() throws IOException { + service.putDirect("bucket", "dir/sub/a.txt", "aaa".getBytes()); + service.putDirect("bucket", "dir/sub/b.txt", "bbb".getBytes()); + String localDir = tempDir.toString() + "/"; + + service.download("bucket", "dir/", localDir, true); + + // Files should appear relative to localDir + assertTrue(Files.exists(tempDir.resolve("sub/a.txt")) || + Files.exists(tempDir.resolve("a.txt"))); + } + + // ── Signed URLs ─────────────────────────────────────────────────────────── + + @Test + void getSignedURL_read_returnsUrl() { + String url = service.getSignedURL("bucket", "key.txt", 3600, "r"); + assertEquals("mem://signed/r/bucket/key.txt?ttl=3600", url); + } + + @Test + void getSignedURL_write_returnsUrl() { + String url = service.getSignedURL("bucket", "key.txt", 3600, "w"); + assertEquals("mem://signed/w/bucket/key.txt?ttl=3600", url); + } + + @Test + void getSignedURLV2_defaultsToRead() { + String url = service.getSignedURLV2("bucket", "key.txt"); + assertTrue(url.contains("bucket") && url.contains("key.txt")); + } + + // ── Inner in-memory implementation ─────────────────────────────────────── + + static class InMemoryStorageService extends AbstractStorageService { + + /** container -> (key -> bytes) */ + private final Map> store = new HashMap<>(); + + boolean exists(String container, String key) { + return store.containsKey(container) && store.get(container).containsKey(key); + } + + void putDirect(String container, String key, byte[] bytes) { + store.computeIfAbsent(container, k -> new HashMap<>()).put(key, bytes); + } + + @Override + protected void ensureContainerExists(String container) { + store.computeIfAbsent(container, k -> new HashMap<>()); + } + + @Override + protected String putObject(String container, String objectKey, java.io.File file) { + try { + byte[] bytes = Files.readAllBytes(file.toPath()); + store.computeIfAbsent(container, k -> new HashMap<>()).put(objectKey, bytes); + return "mem://" + container + "/" + objectKey; + } catch (IOException e) { + throw new StorageServiceException("Failed to put from file: " + e.getMessage(), e); + } + } + + @Override + protected String putObject(String container, String objectKey, byte[] content) { + store.computeIfAbsent(container, k -> new HashMap<>()).put(objectKey, content); + return "mem://" + container + "/" + objectKey; + } + + @Override + protected InputStream getObjectStream(String container, String objectKey) { + Map bucket = store.get(container); + if (bucket == null || !bucket.containsKey(objectKey)) { + throw new StorageServiceException("Not found: " + container + "/" + objectKey); + } + return new ByteArrayInputStream(bucket.get(objectKey)); + } + + @Override + protected String getObjectUri(String container, String objectKey) { + return "mem://" + container + "/" + objectKey; + } + + @Override + protected BlobDetail getObjectDetail(String container, String objectKey) { + Map bucket = store.get(container); + if (bucket == null || !bucket.containsKey(objectKey)) { + throw new StorageServiceException("Not found: " + container + "/" + objectKey); + } + byte[] bytes = bucket.get(objectKey); + Map meta = new HashMap<>(); + meta.put("uri", "mem://" + container + "/" + objectKey); + return new BlobDetail(objectKey, bytes.length, new Date(), meta, + "mem://" + container + "/" + objectKey); + } + + @Override + protected List listKeys(String container, String prefix) { + Map bucket = store.getOrDefault(container, Map.of()); + return bucket.keySet().stream() + .filter(k -> k.startsWith(prefix)) + .sorted() + .collect(Collectors.toList()); + } + + @Override + protected void removeObjects(String container, List objectKeys) { + Map bucket = store.get(container); + if (bucket != null) { + objectKeys.forEach(bucket::remove); + } + } + + @Override + protected void copyObject(String fromContainer, String fromKey, + String toContainer, String toKey) { + Map src = store.get(fromContainer); + if (src == null || !src.containsKey(fromKey)) { + throw new StorageServiceException("Not found: " + fromContainer + "/" + fromKey); + } + store.computeIfAbsent(toContainer, k -> new HashMap<>()).put(toKey, src.get(fromKey)); + } + + @Override + protected String generateSignedGetUrl(String container, String objectKey, int ttlSeconds) { + return "mem://signed/r/" + container + "/" + objectKey + "?ttl=" + ttlSeconds; + } + + @Override + protected String generateSignedPutUrl(String container, String objectKey, + int ttlSeconds, String contentType, + Map additionalParams) { + return "mem://signed/w/" + container + "/" + objectKey + "?ttl=" + ttlSeconds; + } + + @Override + protected String getHdfsPrefix(String container) { + return "mem://" + container + "/"; + } + + @Override + public void close() { + store.clear(); + } + } +} diff --git a/cloud-storage-sdk-api/src/test/java/org/sunbird/cloud/storage/BaseStorageServiceIntegrationTest.java b/cloud-storage-sdk-api/src/test/java/org/sunbird/cloud/storage/BaseStorageServiceIntegrationTest.java new file mode 100644 index 0000000..b5450ee --- /dev/null +++ b/cloud-storage-sdk-api/src/test/java/org/sunbird/cloud/storage/BaseStorageServiceIntegrationTest.java @@ -0,0 +1,289 @@ +package org.sunbird.cloud.storage; + +import org.junit.jupiter.api.*; +import org.junit.jupiter.api.io.TempDir; +import org.sunbird.cloud.storage.model.Blob; +import org.sunbird.cloud.storage.model.DeleteTarget; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; +import java.util.UUID; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * Base class for CSP integration tests. + * + *

Subclasses must override {@link #createService()} to provide a configured + * {@link IStorageService} and {@link #getContainer()} to return a pre-existing + * writable bucket/container name.

+ * + *

Tests are organised to be self-cleaning: each creates objects under a unique + * per-run prefix and removes them in {@code @AfterAll}.

+ */ +public abstract class BaseStorageServiceIntegrationTest { + + @TempDir + protected static Path tempDir; + + protected static IStorageService service; + + /** Unique prefix for this test run to avoid collisions. */ + protected static String testPrefix; + + /** Return a configured storage service. Called once before all tests. */ + protected abstract IStorageService createService(); + + /** Return the name of a pre-existing, writable container/bucket. */ + protected abstract String getContainer(); + + @BeforeAll + void setUpService() { + service = createService(); + testPrefix = "sdk-integration-test/" + UUID.randomUUID() + "/"; + } + + @AfterAll + void tearDown() { + if (service != null) { + try { + // Best-effort cleanup: delete everything under the test prefix + List keys = service.listObjectKeys(getContainer(), testPrefix); + if (!keys.isEmpty()) { + service.deleteObjects(getContainer(), + keys.stream() + .map(DeleteTarget::file) + .toList()); + } + } finally { + service.close(); + } + } + } + + // ── Upload & list ───────────────────────────────────────────────────────── + + @Test + @DisplayName("Upload single file and verify it appears in listing") + void uploadAndList() throws IOException { + Path file = tempDir.resolve("upload-test.txt"); + Files.writeString(file, "integration test content"); + String key = testPrefix + "upload-test.txt"; + + String url = service.upload(getContainer(), file.toString(), key); + + assertNotNull(url, "Upload should return a URL"); + assertFalse(url.isBlank()); + List keys = service.listObjectKeys(getContainer(), testPrefix); + assertTrue(keys.contains(key), "Listed keys should contain the uploaded key"); + } + + @Test + @DisplayName("Upload folder recursively") + void uploadFolder() throws IOException { + Path dir = tempDir.resolve("upload-dir"); + Files.createDirectory(dir); + Files.writeString(dir.resolve("a.txt"), "aaa"); + Files.writeString(dir.resolve("b.txt"), "bbb"); + String folderPrefix = testPrefix + "folder/"; + + service.upload(getContainer(), dir.toString(), folderPrefix, true, 1, 0, null); + + List keys = service.listObjectKeys(getContainer(), folderPrefix); + assertTrue(keys.size() >= 2); + } + + // ── put() (bytes) ───────────────────────────────────────────────────────── + + @Test + @DisplayName("Put bytes directly and verify retrieval") + void putAndGetData() { + String key = testPrefix + "bytes-object.txt"; + byte[] content = "line1\nline2\nline3".getBytes(StandardCharsets.UTF_8); + + service.put(getContainer(), content, key); + + List lines = service.getObjectData(getContainer(), key); + assertEquals(3, lines.size()); + assertEquals("line1", lines.get(0)); + assertEquals("line3", lines.get(2)); + } + + // ── getObject / metadata ────────────────────────────────────────────────── + + @Test + @DisplayName("Get object metadata") + void getObjectMetadata() throws IOException { + Path file = tempDir.resolve("meta-test.txt"); + String content = "metadata test"; + Files.writeString(file, content); + String key = testPrefix + "meta-test.txt"; + service.upload(getContainer(), file.toString(), key); + + Blob blob = service.getObject(getContainer(), key); + + assertEquals(key, blob.getKey()); + assertEquals(content.getBytes().length, blob.getContentLength()); + assertNotNull(blob.getLastModified()); + } + + @Test + @DisplayName("Get object with payload") + void getObjectWithPayload() throws IOException { + Path file = tempDir.resolve("payload-test.txt"); + Files.writeString(file, "payload data"); + String key = testPrefix + "payload-test.txt"; + service.upload(getContainer(), file.toString(), key); + + Blob blob = service.getObject(getContainer(), key, true); + + assertNotNull(blob.getPayload(), "Payload should be present"); + assertTrue(blob.getPayload().length > 0); + } + + // ── Download ────────────────────────────────────────────────────────────── + + @Test + @DisplayName("Download file and verify content") + void downloadFile() throws IOException { + String expected = "download me"; + String key = testPrefix + "download-test.txt"; + service.put(getContainer(), expected.getBytes(StandardCharsets.UTF_8), key); + + Path downloadDir = tempDir.resolve("downloads"); + Files.createDirectories(downloadDir); + service.download(getContainer(), key, downloadDir.toString() + "/"); + + Path downloaded = downloadDir.resolve("download-test.txt"); + assertTrue(Files.exists(downloaded)); + assertEquals(expected, Files.readString(downloaded)); + } + + // ── Signed URLs ─────────────────────────────────────────────────────────── + + @Test + @DisplayName("Generate read signed URL — URL must be non-blank") + void getSignedReadUrl() throws IOException { + Path file = tempDir.resolve("signed-read.txt"); + Files.writeString(file, "signed url test"); + String key = testPrefix + "signed-read.txt"; + service.upload(getContainer(), file.toString(), key); + + String url = service.getSignedURL(getContainer(), key, 3600, "r"); + + assertNotNull(url); + assertFalse(url.isBlank()); + // URL should reference the container and key in some form + assertTrue(url.contains(getContainer()) || url.contains(key) || url.startsWith("https://"), + "Signed URL should be a valid HTTPS URL"); + } + + @Test + @DisplayName("Generate write signed URL — URL must be non-blank") + void getSignedWriteUrl() { + String key = testPrefix + "signed-write.txt"; + String url = service.getSignedURL(getContainer(), key, 3600, "w"); + + assertNotNull(url); + assertFalse(url.isBlank()); + } + + @Test + @DisplayName("getSignedURLV2 — supports content type and additional params") + void getSignedUrlV2() throws IOException { + Path file = tempDir.resolve("signed-v2.txt"); + Files.writeString(file, "v2 test"); + String key = testPrefix + "signed-v2.txt"; + service.upload(getContainer(), file.toString(), key); + + String url = service.getSignedURLV2(getContainer(), key, 3600, "r", + "text/plain", null); + + assertNotNull(url); + assertFalse(url.isBlank()); + } + + // ── Copy ───────────────────────────────────────────────────────────────── + + @Test + @DisplayName("Copy single object within the same container") + void copyObject() throws IOException { + Path file = tempDir.resolve("copy-src.txt"); + Files.writeString(file, "copy source"); + String srcKey = testPrefix + "copy-src.txt"; + String dstKey = testPrefix + "copy-dst.txt"; + service.upload(getContainer(), file.toString(), srcKey); + + service.copyObjects(getContainer(), srcKey, getContainer(), dstKey); + + List keys = service.listObjectKeys(getContainer(), testPrefix); + assertTrue(keys.contains(srcKey), "Source key should still exist"); + assertTrue(keys.contains(dstKey), "Destination key should exist"); + } + + // ── Delete ──────────────────────────────────────────────────────────────── + + @Test + @DisplayName("Delete object and verify removal") + void deleteObject() throws IOException { + Path file = tempDir.resolve("delete-test.txt"); + Files.writeString(file, "to be deleted"); + String key = testPrefix + "delete-test.txt"; + service.upload(getContainer(), file.toString(), key); + assertTrue(service.listObjectKeys(getContainer(), key).contains(key)); + + service.deleteObject(getContainer(), key); + + List keys = service.listObjectKeys(getContainer(), key); + assertFalse(keys.contains(key), "Deleted key should not appear in listing"); + } + + // ── Search ──────────────────────────────────────────────────────────────── + + @Test + @DisplayName("Search objects by date prefix") + void searchObjectsByDate() { + String datePrefix = testPrefix + "dated/"; + service.put(getContainer(), "d1".getBytes(), datePrefix + "2024-06-01/file.txt"); + service.put(getContainer(), "d2".getBytes(), datePrefix + "2024-06-02/file.txt"); + service.put(getContainer(), "d3".getBytes(), datePrefix + "2024-06-10/file.txt"); + + List results = service.searchObjects( + getContainer(), datePrefix, "2024-06-01", "2024-06-02", null, "yyyy-MM-dd"); + + assertEquals(2, results.size()); + } + + @Test + @DisplayName("Search object keys by delta") + void searchObjectKeysByDelta() { + String datePrefix = testPrefix + "delta/"; + service.put(getContainer(), "d1".getBytes(), datePrefix + "2024-07-08/file.txt"); + service.put(getContainer(), "d2".getBytes(), datePrefix + "2024-07-09/file.txt"); + service.put(getContainer(), "d3".getBytes(), datePrefix + "2024-07-10/file.txt"); + service.put(getContainer(), "d4".getBytes(), datePrefix + "2024-07-20/file.txt"); + + List keys = service.searchObjectKeys( + getContainer(), datePrefix, null, "2024-07-10", 2, "yyyy-MM-dd"); + + assertEquals(3, keys.size()); + } + + // ── HDFS paths ──────────────────────────────────────────────────────────── + + @Test + @DisplayName("getPaths returns HDFS-prefixed paths") + void getPaths() { + Blob blob = new Blob(testPrefix + "some-file.txt", 100, null, java.util.Map.of()); + List paths = service.getPaths(getContainer(), List.of(blob)); + + assertEquals(1, paths.size()); + String path = paths.get(0); + assertTrue(path.contains(getContainer()), "Path should contain the container name"); + assertTrue(path.endsWith(testPrefix + "some-file.txt"), + "Path should end with the blob key"); + } +} diff --git a/cloud-storage-sdk-api/src/test/java/org/sunbird/cloud/storage/util/DateRangeUtilTest.java b/cloud-storage-sdk-api/src/test/java/org/sunbird/cloud/storage/util/DateRangeUtilTest.java new file mode 100644 index 0000000..625f6a6 --- /dev/null +++ b/cloud-storage-sdk-api/src/test/java/org/sunbird/cloud/storage/util/DateRangeUtilTest.java @@ -0,0 +1,69 @@ +package org.sunbird.cloud.storage.util; + +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.*; + +class DateRangeUtilTest { + + @Test + void getStartDate_subtractsDeltaFromEndDate() { + String result = DateRangeUtil.getStartDate("2024-05-10", 5); + assertEquals("2024-05-05", result); + } + + @Test + void getStartDate_nullEndDateUsesToday() { + // Just assert it returns a valid date string without throwing + String result = DateRangeUtil.getStartDate(null, 3); + assertNotNull(result); + assertTrue(result.matches("\\d{4}-\\d{2}-\\d{2}")); + } + + @Test + void getStartDate_emptyEndDateUsesToday() { + String result = DateRangeUtil.getStartDate("", 7); + assertNotNull(result); + assertTrue(result.matches("\\d{4}-\\d{2}-\\d{2}")); + } + + @Test + void getDatesBetween_inclusiveDateRange() { + String[] dates = DateRangeUtil.getDatesBetween("2024-03-01", "2024-03-05", "yyyy-MM-dd"); + assertEquals(5, dates.length); + assertEquals("2024-03-01", dates[0]); + assertEquals("2024-03-05", dates[4]); + } + + @Test + void getDatesBetween_singleDayRange() { + String[] dates = DateRangeUtil.getDatesBetween("2024-06-15", "2024-06-15", "yyyy-MM-dd"); + assertEquals(1, dates.length); + assertEquals("2024-06-15", dates[0]); + } + + @Test + void getDatesBetween_nullToDateUsesToday() { + // fromDate = yesterday relative to today; result should have >= 1 element + String yesterday = DateRangeUtil.getStartDate(null, 1); + String[] dates = DateRangeUtil.getDatesBetween(yesterday, null, "yyyy-MM-dd"); + assertTrue(dates.length >= 1); + } + + @Test + void getDatesBetween_customPattern() { + String[] dates = DateRangeUtil.getDatesBetween("01/03/2024", "03/03/2024", "dd/MM/yyyy"); + assertEquals(3, dates.length); + assertEquals("01/03/2024", dates[0]); + assertEquals("03/03/2024", dates[2]); + } + + @Test + void getDatesBetween_containsConsecutiveDates() { + String[] dates = DateRangeUtil.getDatesBetween("2024-01-30", "2024-02-02", "yyyy-MM-dd"); + assertEquals(4, dates.length); + assertEquals("2024-01-30", dates[0]); + assertEquals("2024-01-31", dates[1]); + assertEquals("2024-02-01", dates[2]); + assertEquals("2024-02-02", dates[3]); + } +} diff --git a/cloud-storage-sdk-api/src/test/java/org/sunbird/cloud/storage/util/FileUtilTest.java b/cloud-storage-sdk-api/src/test/java/org/sunbird/cloud/storage/util/FileUtilTest.java new file mode 100644 index 0000000..9512c9d --- /dev/null +++ b/cloud-storage-sdk-api/src/test/java/org/sunbird/cloud/storage/util/FileUtilTest.java @@ -0,0 +1,124 @@ +package org.sunbird.cloud.storage.util; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import java.io.*; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; +import java.util.zip.ZipEntry; +import java.util.zip.ZipOutputStream; + +import static org.junit.jupiter.api.Assertions.*; + +class FileUtilTest { + + @TempDir + Path tempDir; + + @Test + void listFilesRecursive_returnsEmptyForNonExistentDir() { + List files = FileUtil.listFilesRecursive(new File("/nonexistent/path")); + assertTrue(files.isEmpty()); + } + + @Test + void listFilesRecursive_returnsSingleFile() throws IOException { + File f = tempDir.resolve("file.txt").toFile(); + f.createNewFile(); + List files = FileUtil.listFilesRecursive(f); + assertEquals(1, files.size()); + assertEquals(f, files.get(0)); + } + + @Test + void listFilesRecursive_recursesSubdirectories() throws IOException { + Path sub = tempDir.resolve("sub"); + Files.createDirectory(sub); + Files.writeString(tempDir.resolve("root.txt"), "root"); + Files.writeString(sub.resolve("child.txt"), "child"); + + List files = FileUtil.listFilesRecursive(tempDir.toFile()); + assertEquals(2, files.size()); + assertTrue(files.stream().anyMatch(f -> f.getName().equals("root.txt"))); + assertTrue(files.stream().anyMatch(f -> f.getName().equals("child.txt"))); + } + + @Test + void listFilesRecursive_ignoresDirectoriesThemselves() throws IOException { + Path sub = Files.createDirectory(tempDir.resolve("emptydir")); + // directory exists but no files + List files = FileUtil.listFilesRecursive(tempDir.toFile()); + assertTrue(files.isEmpty()); + } + + @Test + void copyStream_writesFileToDestination() throws IOException { + String content = "hello cloud storage"; + InputStream is = new ByteArrayInputStream(content.getBytes(StandardCharsets.UTF_8)); + String destDir = tempDir.toString() + "/"; + + FileUtil.copyStream(is, destDir, "output.txt"); + + Path output = tempDir.resolve("output.txt"); + assertTrue(Files.exists(output)); + assertEquals(content, Files.readString(output)); + } + + @Test + void copyStream_createsDirectoryIfAbsent() throws IOException { + String content = "test content"; + InputStream is = new ByteArrayInputStream(content.getBytes(StandardCharsets.UTF_8)); + String newDir = tempDir.resolve("newsubdir").toString() + "/"; + + FileUtil.copyStream(is, newDir, "file.txt"); + + assertTrue(Files.exists(Path.of(newDir, "file.txt"))); + } + + @Test + void unZip_extractsFilesCorrectly() throws IOException { + // Create a test zip + Path zipFile = tempDir.resolve("test.zip"); + try (ZipOutputStream zos = new ZipOutputStream(new FileOutputStream(zipFile.toFile()))) { + zos.putNextEntry(new ZipEntry("data/file1.txt")); + zos.write("content1".getBytes(StandardCharsets.UTF_8)); + zos.closeEntry(); + zos.putNextEntry(new ZipEntry("data/file2.txt")); + zos.write("content2".getBytes(StandardCharsets.UTF_8)); + zos.closeEntry(); + } + + Path outputDir = tempDir.resolve("extracted"); + FileUtil.unZip(zipFile.toString(), outputDir.toString()); + + assertTrue(Files.exists(outputDir.resolve("data/file1.txt"))); + assertTrue(Files.exists(outputDir.resolve("data/file2.txt"))); + assertEquals("content1", Files.readString(outputDir.resolve("data/file1.txt"))); + } + + @Test + void unZip_skipsHiddenAndUnderscoreFiles() throws IOException { + Path zipFile = tempDir.resolve("test.zip"); + try (ZipOutputStream zos = new ZipOutputStream(new FileOutputStream(zipFile.toFile()))) { + zos.putNextEntry(new ZipEntry(".hidden")); + zos.write("hidden".getBytes(StandardCharsets.UTF_8)); + zos.closeEntry(); + zos.putNextEntry(new ZipEntry("_metadata")); + zos.write("meta".getBytes(StandardCharsets.UTF_8)); + zos.closeEntry(); + zos.putNextEntry(new ZipEntry("real.txt")); + zos.write("real".getBytes(StandardCharsets.UTF_8)); + zos.closeEntry(); + } + + Path outputDir = tempDir.resolve("out"); + FileUtil.unZip(zipFile.toString(), outputDir.toString()); + + assertFalse(Files.exists(outputDir.resolve(".hidden"))); + assertFalse(Files.exists(outputDir.resolve("_metadata"))); + assertTrue(Files.exists(outputDir.resolve("real.txt"))); + } +} diff --git a/cloud-storage-sdk-aws/pom.xml b/cloud-storage-sdk-aws/pom.xml index 803c473..ea3c22e 100644 --- a/cloud-storage-sdk-aws/pom.xml +++ b/cloud-storage-sdk-aws/pom.xml @@ -26,6 +26,12 @@ software.amazon.awssdk sts + + org.sunbird + cloud-storage-sdk-api + tests + test + org.junit.jupiter junit-jupiter diff --git a/cloud-storage-sdk-aws/src/test/java/org/sunbird/cloud/storage/service/aws/AwsStorageServiceTest.java b/cloud-storage-sdk-aws/src/test/java/org/sunbird/cloud/storage/service/aws/AwsStorageServiceTest.java new file mode 100644 index 0000000..bc7556d --- /dev/null +++ b/cloud-storage-sdk-aws/src/test/java/org/sunbird/cloud/storage/service/aws/AwsStorageServiceTest.java @@ -0,0 +1,89 @@ +package org.sunbird.cloud.storage.service.aws; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.TestInstance; +import org.sunbird.cloud.storage.BaseStorageServiceIntegrationTest; +import org.sunbird.cloud.storage.IStorageService; +import org.sunbird.cloud.storage.StorageConfig; + +import static org.junit.jupiter.api.Assumptions.assumeTrue; + +/** + * AWS S3 integration tests. + * + *

Tests are skipped by default and only execute when the following environment + * variables are set:

+ *
    + *
  • {@code AWS_STORAGE_TEST_ENABLED} — any non-null value enables the tests
  • + *
  • {@code AWS_STORAGE_TEST_CONTAINER} — name of a pre-existing, writable S3 bucket
  • + *
+ * + *

Authentication is determined by {@code AWS_STORAGE_TEST_AUTH_TYPE}:

+ *
    + *
  • {@code ACCESS_KEY} (default) — reads {@code AWS_ACCESS_KEY_ID} and + * {@code AWS_SECRET_ACCESS_KEY}
  • + *
  • {@code IAM} / {@code IAM_ROLE} / {@code INSTANCE_PROFILE} — uses the + * default AWS credential chain (instance profile, task role, etc.)
  • + *
+ * + *

Optional: {@code AWS_REGION} (default: {@code us-east-1})

+ * + *

For Ceph S3-compatible endpoints, also set:

+ *
    + *
  • {@code AWS_STORAGE_TEST_ENDPOINT} — custom endpoint URL
  • + *
  • {@code AWS_STORAGE_TEST_TYPE=CEPHS3}
  • + *
+ */ +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +class AwsStorageServiceTest extends BaseStorageServiceIntegrationTest { + + @Override + protected IStorageService createService() { + assumeTrue(System.getenv("AWS_STORAGE_TEST_ENABLED") != null, + "Skipping AWS integration tests: set AWS_STORAGE_TEST_ENABLED=true to enable"); + + String authTypeStr = getEnv("AWS_STORAGE_TEST_AUTH_TYPE", "ACCESS_KEY"); + StorageConfig.AuthType authType = StorageConfig.AuthType.valueOf(authTypeStr.toUpperCase()); + + String storageTypeStr = getEnv("AWS_STORAGE_TEST_TYPE", "AWS"); + StorageConfig.StorageType storageType = StorageConfig.StorageType.valueOf(storageTypeStr.toUpperCase()); + + StorageConfig.Builder builder = StorageConfig.builder(storageType) + .authType(authType) + .region(getEnv("AWS_REGION", "us-east-1")); + + if (authType == StorageConfig.AuthType.ACCESS_KEY) { + String key = System.getenv("AWS_ACCESS_KEY_ID"); + String secret = System.getenv("AWS_SECRET_ACCESS_KEY"); + assumeTrue(key != null && secret != null, + "Skipping: AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY must be set for ACCESS_KEY auth"); + builder.storageKey(key).storageSecret(secret); + } + + String endpoint = System.getenv("AWS_STORAGE_TEST_ENDPOINT"); + if (endpoint != null && !endpoint.isBlank()) { + builder.endPoint(endpoint); + } + + return new AwsStorageService(builder.build()); + } + + @Override + protected String getContainer() { + String container = System.getenv("AWS_STORAGE_TEST_CONTAINER"); + assumeTrue(container != null && !container.isBlank(), + "Skipping: AWS_STORAGE_TEST_CONTAINER must specify a pre-existing writable bucket"); + return container; + } + + @BeforeAll + static void checkAwsEnabled() { + assumeTrue(System.getenv("AWS_STORAGE_TEST_ENABLED") != null, + "Skipping AWS integration tests: set AWS_STORAGE_TEST_ENABLED=true to enable"); + } + + private static String getEnv(String name, String defaultValue) { + String value = System.getenv(name); + return (value != null && !value.isBlank()) ? value : defaultValue; + } +} diff --git a/cloud-storage-sdk-azure/pom.xml b/cloud-storage-sdk-azure/pom.xml index 223dce0..acc0a73 100644 --- a/cloud-storage-sdk-azure/pom.xml +++ b/cloud-storage-sdk-azure/pom.xml @@ -26,6 +26,12 @@ com.azure azure-identity
+ + org.sunbird + cloud-storage-sdk-api + tests + test + org.junit.jupiter junit-jupiter diff --git a/cloud-storage-sdk-azure/src/test/java/org/sunbird/cloud/storage/service/azure/AzureStorageServiceTest.java b/cloud-storage-sdk-azure/src/test/java/org/sunbird/cloud/storage/service/azure/AzureStorageServiceTest.java new file mode 100644 index 0000000..ad130d6 --- /dev/null +++ b/cloud-storage-sdk-azure/src/test/java/org/sunbird/cloud/storage/service/azure/AzureStorageServiceTest.java @@ -0,0 +1,78 @@ +package org.sunbird.cloud.storage.service.azure; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.TestInstance; +import org.sunbird.cloud.storage.BaseStorageServiceIntegrationTest; +import org.sunbird.cloud.storage.IStorageService; +import org.sunbird.cloud.storage.StorageConfig; + +import static org.junit.jupiter.api.Assumptions.assumeTrue; + +/** + * Azure Blob Storage integration tests. + * + *

Tests are skipped by default and only execute when the following environment + * variables are set:

+ *
    + *
  • {@code AZURE_STORAGE_TEST_ENABLED} — any non-null value enables the tests
  • + *
  • {@code AZURE_STORAGE_TEST_CONTAINER} — name of a pre-existing, writable container
  • + *
  • {@code AZURE_STORAGE_ACCOUNT_NAME} — storage account name
  • + *
+ * + *

Authentication is determined by {@code AZURE_STORAGE_TEST_AUTH_TYPE}:

+ *
    + *
  • {@code ACCESS_KEY} (default) — reads {@code AZURE_STORAGE_ACCOUNT_KEY}
  • + *
  • {@code OIDC} — uses Workload Identity; requires {@code AZURE_CLIENT_ID}, + * {@code AZURE_TENANT_ID}, {@code AZURE_FEDERATED_TOKEN_FILE} or falls back to + * {@code DefaultAzureCredential}
  • + *
  • {@code IAM} / {@code INSTANCE_PROFILE} — uses Managed Identity
  • + *
+ */ +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +class AzureStorageServiceTest extends BaseStorageServiceIntegrationTest { + + @Override + protected IStorageService createService() { + assumeTrue(System.getenv("AZURE_STORAGE_TEST_ENABLED") != null, + "Skipping Azure integration tests: set AZURE_STORAGE_TEST_ENABLED=true to enable"); + + String accountName = System.getenv("AZURE_STORAGE_ACCOUNT_NAME"); + assumeTrue(accountName != null && !accountName.isBlank(), + "Skipping: AZURE_STORAGE_ACCOUNT_NAME must be set"); + + String authTypeStr = getEnv("AZURE_STORAGE_TEST_AUTH_TYPE", "ACCESS_KEY"); + StorageConfig.AuthType authType = StorageConfig.AuthType.valueOf(authTypeStr.toUpperCase()); + + StorageConfig.Builder builder = StorageConfig.builder(StorageConfig.StorageType.AZURE) + .authType(authType) + .storageKey(accountName); + + if (authType == StorageConfig.AuthType.ACCESS_KEY) { + String accountKey = System.getenv("AZURE_STORAGE_ACCOUNT_KEY"); + assumeTrue(accountKey != null && !accountKey.isBlank(), + "Skipping: AZURE_STORAGE_ACCOUNT_KEY must be set for ACCESS_KEY auth"); + builder.storageSecret(accountKey); + } + + return new AzureStorageService(builder.build()); + } + + @Override + protected String getContainer() { + String container = System.getenv("AZURE_STORAGE_TEST_CONTAINER"); + assumeTrue(container != null && !container.isBlank(), + "Skipping: AZURE_STORAGE_TEST_CONTAINER must specify a pre-existing writable container"); + return container; + } + + @BeforeAll + static void checkAzureEnabled() { + assumeTrue(System.getenv("AZURE_STORAGE_TEST_ENABLED") != null, + "Skipping Azure integration tests: set AZURE_STORAGE_TEST_ENABLED=true to enable"); + } + + private static String getEnv(String name, String defaultValue) { + String value = System.getenv(name); + return (value != null && !value.isBlank()) ? value : defaultValue; + } +} diff --git a/cloud-storage-sdk-gcp/pom.xml b/cloud-storage-sdk-gcp/pom.xml index f36566c..289ba95 100644 --- a/cloud-storage-sdk-gcp/pom.xml +++ b/cloud-storage-sdk-gcp/pom.xml @@ -22,6 +22,12 @@ com.google.cloud google-cloud-storage
+ + org.sunbird + cloud-storage-sdk-api + tests + test + org.junit.jupiter junit-jupiter diff --git a/cloud-storage-sdk-gcp/src/test/java/org/sunbird/cloud/storage/service/gcp/GcpStorageServiceTest.java b/cloud-storage-sdk-gcp/src/test/java/org/sunbird/cloud/storage/service/gcp/GcpStorageServiceTest.java new file mode 100644 index 0000000..96cb637 --- /dev/null +++ b/cloud-storage-sdk-gcp/src/test/java/org/sunbird/cloud/storage/service/gcp/GcpStorageServiceTest.java @@ -0,0 +1,78 @@ +package org.sunbird.cloud.storage.service.gcp; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.TestInstance; +import org.sunbird.cloud.storage.BaseStorageServiceIntegrationTest; +import org.sunbird.cloud.storage.IStorageService; +import org.sunbird.cloud.storage.StorageConfig; + +import static org.junit.jupiter.api.Assumptions.assumeTrue; + +/** + * Google Cloud Storage integration tests. + * + *

Tests are skipped by default and only execute when the following environment + * variables are set:

+ *
    + *
  • {@code GCP_STORAGE_TEST_ENABLED} — any non-null value enables the tests
  • + *
  • {@code GCP_STORAGE_TEST_CONTAINER} — name of a pre-existing, writable GCS bucket
  • + *
+ * + *

Authentication is determined by {@code GCP_STORAGE_TEST_AUTH_TYPE}:

+ *
    + *
  • {@code ACCESS_KEY} (default) — reads {@code GCP_SERVICE_ACCOUNT_KEY_JSON} + * (the full JSON content of a service account key file)
  • + *
  • {@code OIDC} / {@code IAM} — uses Application Default Credentials + * ({@code GOOGLE_APPLICATION_CREDENTIALS} or GCE metadata)
  • + *
+ * + *

Optional: {@code GCP_STORAGE_TEST_PROJECT} — GCP project ID (used for some operations)

+ */ +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +class GcpStorageServiceTest extends BaseStorageServiceIntegrationTest { + + @Override + protected IStorageService createService() { + assumeTrue(System.getenv("GCP_STORAGE_TEST_ENABLED") != null, + "Skipping GCP integration tests: set GCP_STORAGE_TEST_ENABLED=true to enable"); + + String authTypeStr = getEnv("GCP_STORAGE_TEST_AUTH_TYPE", "ACCESS_KEY"); + StorageConfig.AuthType authType = StorageConfig.AuthType.valueOf(authTypeStr.toUpperCase()); + + StorageConfig.Builder builder = StorageConfig.builder(StorageConfig.StorageType.GCLOUD) + .authType(authType); + + if (authType == StorageConfig.AuthType.ACCESS_KEY) { + String serviceAccountJson = System.getenv("GCP_SERVICE_ACCOUNT_KEY_JSON"); + assumeTrue(serviceAccountJson != null && !serviceAccountJson.isBlank(), + "Skipping: GCP_SERVICE_ACCOUNT_KEY_JSON must contain the service account key JSON for ACCESS_KEY auth"); + builder.storageSecret(serviceAccountJson); + } + + String project = System.getenv("GCP_STORAGE_TEST_PROJECT"); + if (project != null && !project.isBlank()) { + builder.storageKey(project); + } + + return new GcpStorageService(builder.build()); + } + + @Override + protected String getContainer() { + String container = System.getenv("GCP_STORAGE_TEST_CONTAINER"); + assumeTrue(container != null && !container.isBlank(), + "Skipping: GCP_STORAGE_TEST_CONTAINER must specify a pre-existing writable GCS bucket"); + return container; + } + + @BeforeAll + static void checkGcpEnabled() { + assumeTrue(System.getenv("GCP_STORAGE_TEST_ENABLED") != null, + "Skipping GCP integration tests: set GCP_STORAGE_TEST_ENABLED=true to enable"); + } + + private static String getEnv(String name, String defaultValue) { + String value = System.getenv(name); + return (value != null && !value.isBlank()) ? value : defaultValue; + } +} diff --git a/cloud-storage-sdk-oci/pom.xml b/cloud-storage-sdk-oci/pom.xml index f2c4777..f89d848 100644 --- a/cloud-storage-sdk-oci/pom.xml +++ b/cloud-storage-sdk-oci/pom.xml @@ -27,6 +27,12 @@ oci-java-sdk-common-httpclient-jersey3 runtime
+ + org.sunbird + cloud-storage-sdk-api + tests + test + org.junit.jupiter junit-jupiter diff --git a/cloud-storage-sdk-oci/src/test/java/org/sunbird/cloud/storage/service/oci/OciStorageServiceTest.java b/cloud-storage-sdk-oci/src/test/java/org/sunbird/cloud/storage/service/oci/OciStorageServiceTest.java new file mode 100644 index 0000000..cd19029 --- /dev/null +++ b/cloud-storage-sdk-oci/src/test/java/org/sunbird/cloud/storage/service/oci/OciStorageServiceTest.java @@ -0,0 +1,89 @@ +package org.sunbird.cloud.storage.service.oci; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.TestInstance; +import org.sunbird.cloud.storage.BaseStorageServiceIntegrationTest; +import org.sunbird.cloud.storage.IStorageService; +import org.sunbird.cloud.storage.StorageConfig; + +import static org.junit.jupiter.api.Assumptions.assumeTrue; + +/** + * Oracle Cloud Infrastructure Object Storage integration tests. + * + *

Tests are skipped by default and only execute when the following environment + * variables are set:

+ *
    + *
  • {@code OCI_STORAGE_TEST_ENABLED} — any non-null value enables the tests
  • + *
  • {@code OCI_STORAGE_TEST_CONTAINER} — name of a pre-existing, writable OCI bucket
  • + *
+ * + *

Authentication is determined by {@code OCI_STORAGE_TEST_AUTH_TYPE}:

+ *
    + *
  • {@code ACCESS_KEY} (default) — API Key auth; requires: + *
      + *
    • {@code OCI_TENANCY_ID} — tenancy OCID
    • + *
    • {@code OCI_USER_ID} — user OCID (mapped to storageKey)
    • + *
    • {@code OCI_FINGERPRINT} — key fingerprint
    • + *
    • {@code OCI_PRIVATE_KEY_PEM} — PEM content of the private key (mapped to storageSecret)
    • + *
    • {@code OCI_REGION} — OCI region identifier (e.g. {@code us-ashburn-1})
    • + *
    + *
  • + *
  • {@code INSTANCE_PROFILE} / {@code IAM} — uses Instance Principals + * (for OCI Compute instances or OKE pods)
  • + *
+ */ +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +class OciStorageServiceTest extends BaseStorageServiceIntegrationTest { + + @Override + protected IStorageService createService() { + assumeTrue(System.getenv("OCI_STORAGE_TEST_ENABLED") != null, + "Skipping OCI integration tests: set OCI_STORAGE_TEST_ENABLED=true to enable"); + + String authTypeStr = getEnv("OCI_STORAGE_TEST_AUTH_TYPE", "ACCESS_KEY"); + StorageConfig.AuthType authType = StorageConfig.AuthType.valueOf(authTypeStr.toUpperCase()); + + StorageConfig.Builder builder = StorageConfig.builder(StorageConfig.StorageType.OCI) + .authType(authType) + .region(getEnv("OCI_REGION", "us-ashburn-1")); + + if (authType == StorageConfig.AuthType.ACCESS_KEY) { + String userId = System.getenv("OCI_USER_ID"); + String privateKeyPem = System.getenv("OCI_PRIVATE_KEY_PEM"); + + assumeTrue(userId != null && !userId.isBlank(), + "Skipping: OCI_USER_ID must be set for ACCESS_KEY auth"); + assumeTrue(privateKeyPem != null && !privateKeyPem.isBlank(), + "Skipping: OCI_PRIVATE_KEY_PEM must contain the private key PEM for ACCESS_KEY auth"); + assumeTrue(System.getenv("OCI_TENANCY_ID") != null, + "Skipping: OCI_TENANCY_ID must be set for ACCESS_KEY auth"); + assumeTrue(System.getenv("OCI_FINGERPRINT") != null, + "Skipping: OCI_FINGERPRINT must be set for ACCESS_KEY auth"); + + // storageKey = user OCID, storageSecret = PEM content (as expected by OciStorageService) + builder.storageKey(userId).storageSecret(privateKeyPem); + } + + return new OciStorageService(builder.build()); + } + + @Override + protected String getContainer() { + String container = System.getenv("OCI_STORAGE_TEST_CONTAINER"); + assumeTrue(container != null && !container.isBlank(), + "Skipping: OCI_STORAGE_TEST_CONTAINER must specify a pre-existing writable bucket"); + return container; + } + + @BeforeAll + static void checkOciEnabled() { + assumeTrue(System.getenv("OCI_STORAGE_TEST_ENABLED") != null, + "Skipping OCI integration tests: set OCI_STORAGE_TEST_ENABLED=true to enable"); + } + + private static String getEnv(String name, String defaultValue) { + String value = System.getenv(name); + return (value != null && !value.isBlank()) ? value : defaultValue; + } +} From 820eb0371215094ff024688ccf5527b749d8efb8 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 20 Feb 2026 05:13:25 +0000 Subject: [PATCH 28/47] docs: Add CLAUDE.md with full agent context for this repository Captures module layout, key source files, StorageConfig/AuthType enums, the 12 AbstractStorageService primitives, per-CSP auth patterns, service discovery via ServiceLoader, test strategy, build commands, package structure, and common modification patterns. https://claude.ai/code/session_01KrRXhCQDoUCac99315Fnh6 --- CLAUDE.md | 306 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 306 insertions(+) create mode 100644 CLAUDE.md diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..559c192 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,306 @@ +# CLAUDE.md — Sunbird Cloud Storage SDK + +This file gives Claude agents the essential context needed to work on this +repository without reading every source file first. + +--- + +## What this repository is + +A **multi-module Java 11 Maven SDK** for cloud object storage. It replaced a +single-module Scala/jclouds implementation (v1.x) with clean Java 11 modules +that use each CSP's official SDK and support OIDC-based workload identity as +the primary authentication approach. + +**Current version: 2.0.0** + +--- + +## Module layout + +``` +sunbird-cloud-storage-sdk/ ← parent POM (packaging=pom) +├── cloud-storage-sdk-api/ ← interface, models, shared abstractions (no CSP deps) +├── cloud-storage-sdk-aws/ ← AWS S3 + Ceph S3-compatible (AWS SDK v2) +├── cloud-storage-sdk-azure/ ← Azure Blob Storage (Azure SDK for Java) +├── cloud-storage-sdk-gcp/ ← Google Cloud Storage (Google Cloud Java SDK) +└── cloud-storage-sdk-oci/ ← OCI Object Storage (OCI Java SDK) +``` + +All CSP modules depend on `cloud-storage-sdk-api`. There are no +cross-dependencies between CSP modules. + +--- + +## Key source files + +### API module (`cloud-storage-sdk-api`) + +| File | Purpose | +|---|---| +| `IStorageService.java` | Main public interface (19 methods + default overloads), extends `AutoCloseable` | +| `StorageConfig.java` | Builder-pattern config; holds `AuthType` and `StorageType` enums | +| `AbstractStorageService.java` | 12 abstract primitives + all orchestration (retry, folder upload, date search, archive extract) | +| `StorageServiceFactory.java` | `ServiceLoader`-based factory: `getStorageService(StorageConfig)` | +| `StorageServiceProvider.java` | SPI interface each CSP implements | +| `model/Blob.java` | Immutable blob metadata (key, contentLength, lastModified, metadata, payload) | +| `model/DeleteTarget.java` | Typed delete target; static factories `DeleteTarget.file(key)` / `DeleteTarget.directory(key)` | +| `util/DateRangeUtil.java` | Pure `java.time` date helpers: `getStartDate`, `getDatesBetween` | +| `util/FileUtil.java` | `listFilesRecursive`, `copyStream`, `unZip` | +| `exception/StorageServiceException.java` | Unchecked runtime exception wrapping all SDK errors | + +### CSP modules + +Each CSP module contains exactly two production classes: + +``` +/src/main/java/org/sunbird/cloud/storage/service// + StorageService.java ← extends AbstractStorageService + StorageServiceProvider.java ← implements StorageServiceProvider + +/src/main/resources/META-INF/services/ + org.sunbird.cloud.storage.StorageServiceProvider ← SPI registration +``` + +### HDFS prefix per CSP + +| CSP | Prefix format | +|---|---| +| AWS | `s3n:///` | +| Azure | `wasb://@.blob.core.windows.net/` | +| GCP | `gs:///` | +| OCI | `oci://@/` | + +--- + +## StorageConfig + +```java +StorageConfig config = StorageConfig.builder(StorageConfig.StorageType.AWS) + .authType(StorageConfig.AuthType.OIDC) + .region("us-east-1") + .build(); +``` + +### AuthType enum + +| Value | Meaning | +|---|---| +| `ACCESS_KEY` | storageKey + storageSecret (static credentials) | +| `OIDC` | Workload Identity / Web Identity Token | +| `IAM` | Managed Identity / default application credentials | +| `IAM_ROLE` | IAM role (AWS STS assumed role) | +| `INSTANCE_PROFILE` | EC2 instance profile / OCI Instance Principal | + +### StorageType enum + +`AWS`, `AZURE`, `GCLOUD`, `OCI`, `CEPHS3` + +`CEPHS3` is handled by the AWS module with `endpointOverride` + +`pathStyleAccessEnabled(true)`. Set `endPoint` in `StorageConfig`. + +--- + +## AbstractStorageService — the 12 primitives + +CSP subclasses must implement these and nothing else: + +```java +void ensureContainerExists(String container) +String putObject(String container, String objectKey, File file) +String putObject(String container, String objectKey, byte[] content) +InputStream getObjectStream(String container, String objectKey) +String getObjectUri(String container, String objectKey) +BlobDetail getObjectDetail(String container, String objectKey) // inner class +List listKeys(String container, String prefix) +void removeObjects(String container, List objectKeys) +void copyObject(String fromContainer, String fromKey, String toContainer, String toKey) +String generateSignedGetUrl(String container, String objectKey, int ttlSeconds) +String generateSignedPutUrl(String container, String objectKey, int ttlSeconds, + String contentType, Map additionalParams) +String getHdfsPrefix(String container) +``` + +The orchestration in `AbstractStorageService` (retry loops, `uploadFolder`, +`searchObjects` date iteration, `extractArchive`, directory copy) is shared +and must NOT be duplicated in CSP implementations. + +--- + +## Authentication per CSP + +### AWS (`AwsStorageService`) +- `ACCESS_KEY` → `StaticCredentialsProvider(AwsBasicCredentials)` +- `OIDC/IAM/IAM_ROLE/INSTANCE_PROFILE` → `DefaultCredentialsProvider` +- Custom endpoint → `S3ClientBuilder.endpointOverride(URI)` + `pathStyleAccessEnabled(true)` + +### Azure (`AzureStorageService`) +- `ACCESS_KEY` → `StorageSharedKeyCredential(accountName, accountKey)`; `storageKey` = account name, `storageSecret` = account key +- `OIDC` → `WorkloadIdentityCredentialBuilder` (reads `AZURE_CLIENT_ID`, `AZURE_TENANT_ID`, `AZURE_FEDERATED_TOKEN_FILE`) or `DefaultAzureCredentialBuilder` +- `IAM/INSTANCE_PROFILE` → `ManagedIdentityCredentialBuilder` +- Signed URLs: shared-key auth uses `generateSas()`; OIDC/IAM uses `getUserDelegationKey()` + `generateUserDelegationSas()` + +### GCP (`GcpStorageService`) +- `ACCESS_KEY` → `GoogleCredentials.fromStream(new ByteArrayInputStream(secret.getBytes()))` where `storageSecret` is the service account JSON string +- `OIDC/IAM` → `GoogleCredentials.getApplicationDefault()` +- Signed URLs use V4 signatures; PUT URL supports chunked upload via `HttpMethod.POST` + `x-goog-resumable: start` + +### OCI (`OciStorageService`) +- `ACCESS_KEY` → `SimpleAuthenticationDetailsProvider`; `storageKey` = user OCID, `storageSecret` = private key PEM; reads `OCI_TENANCY_ID` and `OCI_FINGERPRINT` from env +- `OIDC/IAM/INSTANCE_PROFILE` → `InstancePrincipalsAuthenticationDetailsProvider` +- OCI namespace is fetched via `getNamespace()` in the constructor and stored +- Signed URLs use OCI Pre-Authenticated Requests (PAR) +- All uploads compute and set `contentMD5` (base64-encoded MD5) + +--- + +## Service discovery (ServiceLoader) + +```java +// From user code — auto-discovers the right CSP: +IStorageService svc = StorageServiceFactory.getStorageService(config); + +// Or directly instantiate a CSP class: +IStorageService svc = new AwsStorageService(config); +``` + +The `META-INF/services/org.sunbird.cloud.storage.StorageServiceProvider` file +in each CSP module registers the provider. When all CSP jars are on the +classpath, `StorageServiceFactory` selects the one whose +`provider.supports(config.getType())` returns `true`. + +--- + +## Tests + +### Unit tests (always run, no cloud access needed) + +Located in `cloud-storage-sdk-api/src/test/`: + +| Class | What it tests | +|---|---| +| `DateRangeUtilTest` | Date arithmetic helpers | +| `FileUtilTest` | File utilities with `@TempDir` | +| `AbstractStorageServiceTest` | All orchestration logic via `InMemoryStorageService` inner class | + +### Integration tests (skipped by default) + +Each CSP module has one integration test class that extends +`BaseStorageServiceIntegrationTest` (published as a test-jar from the API +module). Tests are guarded by `assumeTrue()` and only execute when the +`*_STORAGE_TEST_ENABLED` env var is set. + +| Class | Enable with | +|---|---| +| `AwsStorageServiceTest` | `AWS_STORAGE_TEST_ENABLED=true` | +| `AzureStorageServiceTest` | `AZURE_STORAGE_TEST_ENABLED=true` | +| `GcpStorageServiceTest` | `GCP_STORAGE_TEST_ENABLED=true` | +| `OciStorageServiceTest` | `OCI_STORAGE_TEST_ENABLED=true` | + +See **TESTING.md** for the full list of environment variables and usage examples. + +--- + +## Build + +```bash +# Full build, skip tests +mvn clean package -DskipTests + +# Full build with unit tests (integration tests auto-skip) +mvn clean verify + +# Single module +mvn -pl cloud-storage-sdk-aws package -DskipTests +``` + +### Maven build order (reactor) + +1. `cloud-storage-sdk-parent` (parent POM) +2. `cloud-storage-sdk-api` +3. `cloud-storage-sdk-aws`, `cloud-storage-sdk-azure`, `cloud-storage-sdk-gcp`, `cloud-storage-sdk-oci` (parallel) + +### Key versions (managed in parent POM) + +| Dependency | Version | +|---|---| +| AWS SDK v2 | 2.25.0 | +| Azure Blob | 12.25.0 | +| Azure Identity | 1.11.0 | +| GCP Storage | 2.30.0 | +| OCI SDK | 3.30.0 | +| Apache Tika | 2.9.0 | +| JUnit Jupiter | 5.10.1 | +| Mockito | 5.8.0 | +| Java target | 11 | + +--- + +## Package structure + +``` +org.sunbird.cloud.storage +├── IStorageService (interface) +├── AbstractStorageService (abstract class) +├── StorageConfig (config + AuthType/StorageType enums) +├── StorageServiceFactory (factory via ServiceLoader) +├── StorageServiceProvider (SPI interface) +├── BaseStorageServiceIntegrationTest (abstract integration test base, test-jar) +├── exception/ +│ └── StorageServiceException +├── model/ +│ ├── Blob +│ └── DeleteTarget +├── util/ +│ ├── DateRangeUtil +│ └── FileUtil +└── service/ + ├── aws/ AwsStorageService, AwsStorageServiceProvider + ├── azure/ AzureStorageService, AzureStorageServiceProvider + ├── gcp/ GcpStorageService, GcpStorageServiceProvider + └── oci/ OciStorageService, OciStorageServiceProvider +``` + +--- + +## Common patterns when modifying this codebase + +### Adding a new operation to the interface +1. Add the method signature to `IStorageService.java` (add a `default` overload + if optional params are involved) +2. If the operation needs a new primitive, add the `abstract` method to + `AbstractStorageService.java` and implement it in all four CSP services +3. If orchestration logic is shared, implement it in `AbstractStorageService` + and call the abstract primitive +4. Add unit tests in `AbstractStorageServiceTest` using `InMemoryStorageService` +5. Add integration test coverage in `BaseStorageServiceIntegrationTest` + +### Adding a new CSP +1. Create a new Maven module `cloud-storage-sdk-/` +2. Add it to the parent POM `` list +3. Depend on `cloud-storage-sdk-api` +4. Extend `AbstractStorageService`, implement all 12 primitives +5. Implement `StorageServiceProvider`, register in `META-INF/services/` +6. Add `StorageType.` to the enum in `StorageConfig.java` +7. Write an integration test extending `BaseStorageServiceIntegrationTest` + +### Changing authentication logic +Each CSP's `resolveAuth(StorageConfig)` / auth setup is self-contained in the +CSP's service constructor. Do not share auth logic across CSPs. + +--- + +## Important design decisions + +- **No Scala, no jclouds** — complete clean break at v2.0.0 +- **OIDC first** — `AuthType.OIDC` is the intended production auth; `ACCESS_KEY` + is for local development and legacy environments +- **`Ceph S3` is handled in the AWS module** via `endpointOverride` (not a + separate module) +- **`IStorageService extends AutoCloseable`** — use try-with-resources; `close()` + shuts down the underlying SDK client +- **`DeleteTarget` replaces `(String, Boolean)` tuples** — use + `DeleteTarget.file(key)` or `DeleteTarget.directory(key)` +- **Thread-safety** — retry counters use local variables, not instance fields +- **Test-jar** — `cloud-storage-sdk-api` publishes a `-tests` classifier jar so + CSP modules can extend `BaseStorageServiceIntegrationTest` without duplication From 770dd9a84c904e5e426755c17c57f9f053b24046 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 20 Feb 2026 05:58:02 +0000 Subject: [PATCH 29/47] chore: Remove CircleCI configuration CircleCI is no longer used. Removes .circleci/config.yml and the corresponding reference in PLAN.md Phase 6. https://claude.ai/code/session_01KrRXhCQDoUCac99315Fnh6 --- .circleci/config.yml | 34 ---------------------------------- PLAN.md | 5 ++--- 2 files changed, 2 insertions(+), 37 deletions(-) delete mode 100644 .circleci/config.yml diff --git a/.circleci/config.yml b/.circleci/config.yml deleted file mode 100644 index e4929a1..0000000 --- a/.circleci/config.yml +++ /dev/null @@ -1,34 +0,0 @@ -version: 2.1 -jobs: - cloud-storage-build: - machine: - image: ubuntu-2204:current - steps: - - checkout - - restore_cache: - key: cloud-storage-dependency-cache-{{ checksum "pom.xml" }} - - run: - name: Set up JDK 11 - command: | - sudo apt-get update && sudo apt-get install -y openjdk-11-jdk - export JAVA_HOME=/usr/lib/jvm/java-11-openjdk-amd64 - export PATH=$JAVA_HOME/bin:$PATH - - run: - name: Build - command: mvn clean verify -DskipTests - - save_cache: - key: cloud-storage-dependency-cache-{{ checksum "pom.xml" }} - paths: ~/.m2 - - run: - name: Test - command: mvn test - - run: - name: sonar - command: | - mvn -X sonar:sonar -Dsonar.projectKey=project-sunbird_sunbird-cloud-storage-sdk -Dsonar.host.url=https://sonarcloud.io -Dsonar.organization=project-sunbird - -workflows: - version: 2.1 - workflow: - jobs: - - cloud-storage-build diff --git a/PLAN.md b/PLAN.md index ada3275..9818ec4 100644 --- a/PLAN.md +++ b/PLAN.md @@ -490,9 +490,8 @@ IStorageService service = StorageServiceFactory.getStorageService(config); ### Phase 6: CI/CD and Release 1. Update `.github/workflows/maven-release.yml` for multi-module build -2. Update `.circleci/config.yml` for multi-module build and SonarCloud analysis -3. Configure individual module publishing to Maven Central -4. Update README with architecture overview and migration guide +2. Configure individual module publishing to Maven Central +3. Update README with architecture overview and migration guide --- From 42ce663051395cc798e85189278d3130f3be4418 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 20 Feb 2026 10:10:38 +0000 Subject: [PATCH 30/47] fix: Add test-jar version management and CI/Sonar workflows - Add managed dependency for cloud-storage-sdk-api test-jar classifier in parent POM so CSP modules resolve the version correctly - Pin maven-jar-plugin to 3.3.0 in pluginManagement - Add GitHub Actions CI workflow (ci.yml) for build + test on push/PR - Add GitHub Actions SonarCloud workflow (sonar.yml) for static analysis https://claude.ai/code/session_01KrRXhCQDoUCac99315Fnh6 --- .github/workflows/ci.yml | 29 ++++++++++++++++++++++++++ .github/workflows/sonar.yml | 41 +++++++++++++++++++++++++++++++++++++ pom.xml | 12 +++++++++++ 3 files changed, 82 insertions(+) create mode 100644 .github/workflows/ci.yml create mode 100644 .github/workflows/sonar.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..002966c --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,29 @@ +name: CI Build & Test + +on: + push: + branches: + - main + - master + pull_request: + branches: + - main + - master + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up JDK 11 + uses: actions/setup-java@v4 + with: + java-version: '11' + distribution: 'temurin' + cache: 'maven' + + - name: Build and run tests + run: mvn clean verify diff --git a/.github/workflows/sonar.yml b/.github/workflows/sonar.yml new file mode 100644 index 0000000..3c15d7c --- /dev/null +++ b/.github/workflows/sonar.yml @@ -0,0 +1,41 @@ +name: SonarCloud Analysis + +on: + push: + branches: + - main + - master + pull_request: + branches: + - main + - master + +jobs: + sonar: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Set up JDK 11 + uses: actions/setup-java@v4 + with: + java-version: '11' + distribution: 'temurin' + cache: 'maven' + + - name: Build and run tests + run: mvn clean verify + + - name: SonarCloud analysis + env: + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: > + mvn sonar:sonar + -Dsonar.projectKey=project-sunbird_sunbird-cloud-storage-sdk + -Dsonar.organization=project-sunbird + -Dsonar.host.url=https://sonarcloud.io diff --git a/pom.xml b/pom.xml index 7e4d119..6ee05ab 100644 --- a/pom.xml +++ b/pom.xml @@ -88,6 +88,13 @@ cloud-storage-sdk-api ${project.version}
+ + org.sunbird + cloud-storage-sdk-api + ${project.version} + tests + test + @@ -168,6 +175,11 @@ + + org.apache.maven.plugins + maven-jar-plugin + 3.3.0 + org.apache.maven.plugins maven-compiler-plugin From 32e1a6e534735e2c73c3ddefb577ab5fa5af6e90 Mon Sep 17 00:00:00 2001 From: Mahesh Kumar Gangula <6985261+maheshkumargangula@users.noreply.github.com> Date: Fri, 20 Feb 2026 16:15:29 +0530 Subject: [PATCH 31/47] fix: Build issue fixes for the refactored code from Claude. --- .../sunbird/cloud/storage/service/oci/OciStorageService.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/cloud-storage-sdk-oci/src/main/java/org/sunbird/cloud/storage/service/oci/OciStorageService.java b/cloud-storage-sdk-oci/src/main/java/org/sunbird/cloud/storage/service/oci/OciStorageService.java index bb84582..850363a 100644 --- a/cloud-storage-sdk-oci/src/main/java/org/sunbird/cloud/storage/service/oci/OciStorageService.java +++ b/cloud-storage-sdk-oci/src/main/java/org/sunbird/cloud/storage/service/oci/OciStorageService.java @@ -2,6 +2,7 @@ import com.oracle.bmc.Region; import com.oracle.bmc.auth.AuthenticationDetailsProvider; +import com.oracle.bmc.auth.BasicAuthenticationDetailsProvider; import com.oracle.bmc.auth.InstancePrincipalsAuthenticationDetailsProvider; import com.oracle.bmc.auth.SimpleAuthenticationDetailsProvider; import com.oracle.bmc.objectstorage.ObjectStorageClient; @@ -43,7 +44,7 @@ public class OciStorageService extends AbstractStorageService { public OciStorageService(StorageConfig config) { this.config = config; - AuthenticationDetailsProvider authProvider = resolveAuth(config); + BasicAuthenticationDetailsProvider authProvider = resolveAuth(config); ObjectStorageClient.Builder clientBuilder = ObjectStorageClient.builder(); @@ -64,7 +65,7 @@ public OciStorageService(StorageConfig config) { namespace, config.getAuthType()); } - private AuthenticationDetailsProvider resolveAuth(StorageConfig config) { + private BasicAuthenticationDetailsProvider resolveAuth(StorageConfig config) { switch (config.getAuthType()) { case ACCESS_KEY: // For API Key auth, storageKey is the user OCID, storageSecret is the private key PEM From 60d506e6d293a7bc42a666b7951c6b1eda8747fe Mon Sep 17 00:00:00 2001 From: Mahesh Kumar Gangula <6985261+maheshkumargangula@users.noreply.github.com> Date: Fri, 20 Feb 2026 16:28:58 +0530 Subject: [PATCH 32/47] fix: object key path when uploading directories Compute a relativePath for files under a directory and join it to objectKey with an explicit '/' separator before calling upload. This prevents malformed object keys when objectKey doesn't end with a separator and preserves the directory structure during recursive (sync and async) uploads. The change extracts the relative path calculation and applies it in both upload loops. --- .../org/sunbird/cloud/storage/AbstractStorageService.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/cloud-storage-sdk-api/src/main/java/org/sunbird/cloud/storage/AbstractStorageService.java b/cloud-storage-sdk-api/src/main/java/org/sunbird/cloud/storage/AbstractStorageService.java index bf39413..934f376 100644 --- a/cloud-storage-sdk-api/src/main/java/org/sunbird/cloud/storage/AbstractStorageService.java +++ b/cloud-storage-sdk-api/src/main/java/org/sunbird/cloud/storage/AbstractStorageService.java @@ -104,8 +104,9 @@ public String upload(String container, String file, String objectKey, List files = FileUtil.listFilesRecursive(dir); List urls = new ArrayList<>(); for (File f : files) { - String key = objectKey + f.getAbsolutePath() + String relativePath = f.getAbsolutePath() .substring((dir.getAbsolutePath() + File.separator).length()); + String key = objectKey + "/" + relativePath; urls.add(upload(container, f.getAbsolutePath(), key, false, attempt, maxAttempts, ttl)); } return String.join(",", urls); @@ -150,8 +151,9 @@ public CompletableFuture> uploadFolder(String container, String fil List files = FileUtil.listFilesRecursive(dir); List> futures = new ArrayList<>(); for (File f : files) { - String key = objectKey + f.getAbsolutePath() + String relativePath = f.getAbsolutePath() .substring((dir.getAbsolutePath() + File.separator).length()); + String key = objectKey + "/" + relativePath; futures.add(CompletableFuture.supplyAsync(() -> upload(container, f.getAbsolutePath(), key, false, attempt, retryCount != null ? retryCount : 0, ttl))); From f72d8c49471a2470ef632faf248ddc13dc62e7c2 Mon Sep 17 00:00:00 2001 From: Mahesh Kumar Gangula <6985261+maheshkumargangula@users.noreply.github.com> Date: Fri, 20 Feb 2026 16:34:01 +0530 Subject: [PATCH 33/47] fix: build issue with toList (only on Java 16+). Problem: Used .toList() method which is only available in Java 16+, but project targets Java 11. Solution: Replaced .toList() with .collect(Collectors.toList()) and added Collectors import. --- .../cloud/storage/BaseStorageServiceIntegrationTest.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/cloud-storage-sdk-api/src/test/java/org/sunbird/cloud/storage/BaseStorageServiceIntegrationTest.java b/cloud-storage-sdk-api/src/test/java/org/sunbird/cloud/storage/BaseStorageServiceIntegrationTest.java index b5450ee..a4a140f 100644 --- a/cloud-storage-sdk-api/src/test/java/org/sunbird/cloud/storage/BaseStorageServiceIntegrationTest.java +++ b/cloud-storage-sdk-api/src/test/java/org/sunbird/cloud/storage/BaseStorageServiceIntegrationTest.java @@ -11,6 +11,7 @@ import java.nio.file.Path; import java.util.List; import java.util.UUID; +import java.util.stream.Collectors; import static org.junit.jupiter.api.Assertions.*; @@ -56,7 +57,7 @@ void tearDown() { service.deleteObjects(getContainer(), keys.stream() .map(DeleteTarget::file) - .toList()); + .collect(Collectors.toList())); } } finally { service.close(); From c5a097185ba5478897a877714a5532ccceb6bc10 Mon Sep 17 00:00:00 2001 From: Mahesh Kumar Gangula <6985261+maheshkumargangula@users.noreply.github.com> Date: Fri, 20 Feb 2026 17:53:30 +0530 Subject: [PATCH 34/47] fix: maven deploy changes for the new project structure. --- .github/workflows/maven-release.yml | 5 +--- pom.xml | 39 +++++++++++++++++++++-------- 2 files changed, 29 insertions(+), 15 deletions(-) diff --git a/.github/workflows/maven-release.yml b/.github/workflows/maven-release.yml index afb54b2..c761254 100644 --- a/.github/workflows/maven-release.yml +++ b/.github/workflows/maven-release.yml @@ -47,8 +47,5 @@ jobs: "gpg.passphrase": "${{ secrets.GPG_PASSPHRASE }}" }] - - name: Build all modules - run: mvn clean verify -DskipTests - - name: Deploy to Maven Central - run: mvn deploy -DskipTests + run: mvn deploy -Prelease -DskipTests diff --git a/pom.xml b/pom.xml index 6ee05ab..e65d873 100644 --- a/pom.xml +++ b/pom.xml @@ -56,16 +56,6 @@ scm:git:ssh://github.com/project-sunbird/sunbird-cloud-storage-sdk.git https://github.com/project-sunbird/sunbird-cloud-storage-sdk/tree/master - - - central - https://s01.oss.sonatype.org/content/repositories/snapshots - - - central - https://s01.oss.sonatype.org/service/local/staging/deploy/maven2/ - - @@ -224,7 +214,7 @@ org.apache.maven.plugins maven-gpg-plugin - 1.5 + 3.1.0 sign-artifacts @@ -248,6 +238,7 @@ true central + true @@ -263,4 +254,30 @@ + + + + release + + + + org.apache.maven.plugins + maven-source-plugin + + + org.apache.maven.plugins + maven-javadoc-plugin + + + org.apache.maven.plugins + maven-gpg-plugin + + + org.sonatype.central + central-publishing-maven-plugin + + + + + From 8c68054b86d8c654618219466651f593cef0df91 Mon Sep 17 00:00:00 2001 From: Mahesh Kumar Gangula <6985261+maheshkumargangula@users.noreply.github.com> Date: Sat, 21 Feb 2026 11:35:12 +0530 Subject: [PATCH 35/47] feat: Integration Guide to use the CSP specific impl --- INTEGRATION_GUIDE.md | 1082 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 1082 insertions(+) create mode 100644 INTEGRATION_GUIDE.md diff --git a/INTEGRATION_GUIDE.md b/INTEGRATION_GUIDE.md new file mode 100644 index 0000000..5c98690 --- /dev/null +++ b/INTEGRATION_GUIDE.md @@ -0,0 +1,1082 @@ +# Sunbird Cloud Storage SDK — Technical Integration Guide + +**Version:** 2.0.0  |  **Java:** 11  |  **Build:** Maven +**Frameworks:** Play Framework 2.x · Spring Boot 2.x / 3.x +**Supported CSPs:** Amazon Web Services (S3) · Microsoft Azure (Blob) · Google Cloud Storage · Oracle Cloud (OCI) + +--- + +## Table of Contents + +1. [Overview](#1-overview) +2. [Prerequisites](#2-prerequisites) +3. [Maven Dependency Setup](#3-maven-dependency-setup) + - [3.1 Spring Boot — pom.xml](#31-spring-boot--pomxml) + - [3.2 Play Framework 2.x — build.sbt](#32-play-framework-2x--buildsbt) + - [3.3 Transitive Dependencies per CSP](#33-transitive-dependencies-per-csp) +4. [Configuration](#4-configuration) + - [4.1 StorageConfig Builder](#41-storageconfig-builder) + - [4.2 AuthType Values](#42-authtype-values) + - [4.3 Configuration Examples](#43-configuration-examples) + - [4.4 Spring Boot — application.yml Integration](#44-spring-boot--applicationyml-integration) + - [4.5 Play Framework — application.conf Integration](#45-play-framework--applicationconf-integration) +5. [Usage Examples](#5-usage-examples) + - [5.1 Instantiation via ServiceLoader](#51-instantiation-via-serviceloader) + - [5.2 Upload Operations](#52-upload-operations) + - [5.3 Download Operations](#53-download-operations) + - [5.4 Signed URLs](#54-signed-urls) + - [5.5 List and Search Operations](#55-list-and-search-operations) + - [5.6 Delete Operations](#56-delete-operations) + - [5.7 Copy and Archive Operations](#57-copy-and-archive-operations) +6. [Dockerfile — Multi-Stage Builds per CSP](#6-dockerfile--multi-stage-builds-per-csp) + - [6.1 AWS S3 Dockerfile](#61-aws-s3-dockerfile) + - [6.2 Azure Blob Storage Dockerfile](#62-azure-blob-storage-dockerfile) + - [6.3 Google Cloud Storage Dockerfile](#63-google-cloud-storage-dockerfile) + - [6.4 Oracle Cloud (OCI) Dockerfile](#64-oracle-cloud-oci-dockerfile) + - [6.5 Play Framework Dockerfile (Any CSP)](#65-play-framework-dockerfile-any-csp) +7. [Recommended Docker Image Strategy](#7-recommended-docker-image-strategy) + - [7.1 Option A — Build-Time CSP Selection (Recommended)](#71-option-a--build-time-csp-selection-recommended) + - [7.2 Option B — Single Image with All CSP Modules](#72-option-b--single-image-with-all-csp-modules) + - [7.3 Comparison and Recommendation](#73-comparison-and-recommendation) +8. [Runtime Environment Variables](#8-runtime-environment-variables) +9. [Exception Handling](#9-exception-handling) +10. [Quick Reference](#10-quick-reference) + +--- + +## 1. Overview + +The Sunbird Cloud Storage SDK (v2.0.0) is a **multi-module Java 11 Maven SDK** providing a unified interface to major cloud object storage providers. It replaces the previous Scala/jclouds v1.x implementation with a clean, CSP-agnostic API backed by each provider's official SDK. + +### Architecture + +The SDK is organized as a Maven multi-module project with a strict separation between the API contract and provider implementations: + +| Module | Purpose | +|--------|---------| +| `cloud-storage-sdk-api` | Interface, models, shared orchestration. **No CSP dependencies.** This is the only compile-time dependency your application needs. | +| `cloud-storage-sdk-aws` | AWS S3 + Ceph S3-compatible (AWS SDK v2). Include as `runtime` for AWS/Ceph deployments. | +| `cloud-storage-sdk-azure` | Azure Blob Storage (Azure SDK for Java). Include as `runtime` for Azure deployments. | +| `cloud-storage-sdk-gcp` | Google Cloud Storage (Google Cloud Java SDK). Include as `runtime` for GCP deployments. | +| `cloud-storage-sdk-oci` | OCI Object Storage (OCI Java SDK). Include as `runtime` for OCI deployments. | + +The key design principle is that **your application code depends only on the API module at compile time**. The CSP implementation module is added as a `runtime` dependency and is auto-discovered via Java's `ServiceLoader` — no code changes are needed when switching providers. + +> **OIDC-First Design:** `AuthType.OIDC` workload identity is the intended production authentication method. `ACCESS_KEY` is provided for local development and legacy environments only. In Kubernetes and cloud-native deployments, always prefer OIDC to avoid managing long-lived credentials. + +--- + +## 2. Prerequisites + +| Requirement | Details | +|-------------|---------| +| Java | 11 or later (LTS recommended: 11, 17, 21) | +| Maven | 3.6.3 or later | +| SDK Artifact Repository | `sunbird-cloud-storage-sdk:2.0.0` published to your Maven repository (Nexus, Artifactory, GitHub Packages, etc.) | +| Framework | Play Framework 2.x (Scala/Java) or Spring Boot 2.x / 3.x | +| Docker | 20.10+ for building container images (optional but recommended) | + +--- + +## 3. Maven Dependency Setup + +The SDK uses a **compile/runtime split** pattern: your application declares a `compile` dependency on the API module and a `runtime` dependency on exactly one CSP implementation module. This keeps CSP-specific code off your compile classpath and ensures that only the intended provider is active at runtime. + +### 3.1 Spring Boot — pom.xml + +```xml + + + + + + + org.sunbird + cloud-storage-sdk-api + 2.0.0 + + + + + + + + + org.sunbird + cloud-storage-sdk-aws + 2.0.0 + runtime + + + + + + + + + + + + +``` + +### 3.2 Play Framework 2.x — build.sbt + +For Play 2 projects using SBT, the `% "runtime"` suffix is equivalent to Maven's `runtime` scope: + +```scala +libraryDependencies ++= Seq( + + // ── COMPILE: API contract ───────────────────────────────────────────────── + "org.sunbird" % "cloud-storage-sdk-api" % "2.0.0", + + // ── RUNTIME: Uncomment exactly ONE provider ─────────────────────────────── + + // Option A: AWS S3 / Ceph S3-compatible + "org.sunbird" % "cloud-storage-sdk-aws" % "2.0.0" % "runtime", + + // Option B: Azure Blob Storage + // "org.sunbird" % "cloud-storage-sdk-azure" % "2.0.0" % "runtime", + + // Option C: Google Cloud Storage + // "org.sunbird" % "cloud-storage-sdk-gcp" % "2.0.0" % "runtime", + + // Option D: OCI Object Storage + // "org.sunbird" % "cloud-storage-sdk-oci" % "2.0.0" % "runtime" +) +``` + +> **Why `runtime` scope?** The CSP module is loaded by Java's `ServiceLoader` at runtime. It must be on the classpath but must **not** be on the compile classpath — this prevents your application code from accidentally importing CSP-specific classes, keeping it portable across cloud providers. + +### 3.3 Transitive Dependencies per CSP + +Each runtime implementation module automatically pulls in the following dependencies. You do not need to declare these explicitly: + +| CSP Module | Key Transitive Dependencies | +|------------|----------------------------| +| `cloud-storage-sdk-aws` | `software.amazon.awssdk:s3:2.25.0`, `software.amazon.awssdk:sts:2.25.0` | +| `cloud-storage-sdk-azure` | `com.azure:azure-storage-blob:12.25.0`, `com.azure:azure-identity:1.11.0` | +| `cloud-storage-sdk-gcp` | `com.google.cloud:google-cloud-storage:2.30.0` | +| `cloud-storage-sdk-oci` | `com.oracle.oci.sdk:oci-java-sdk-objectstorage:3.30.0`, `com.oracle.oci.sdk:oci-java-sdk-common-httpclient-jersey3:3.30.0` | + +--- + +## 4. Configuration + +All configuration is expressed through the `StorageConfig` builder. The builder requires a `StorageType` and optionally an `AuthType` plus additional parameters depending on your authentication strategy. + +### 4.1 StorageConfig Builder + +| Method | Description | +|--------|-------------| +| `.builder(StorageType)` | Starts the builder for the given CSP. **Required.** | +| `.authType(AuthType)` | Authentication method. Defaults to `ACCESS_KEY` if not set. | +| `.storageKey(String)` | Access key ID (`ACCESS_KEY`) or user OCID (OCI). | +| `.storageSecret(String)` | Secret access key, private key PEM (OCI), or service-account JSON (GCP). | +| `.region(String)` | Cloud region, e.g. `us-east-1` (AWS), `eastus` (Azure), `us-central1` (GCP), `us-ashburn-1` (OCI). | +| `.endPoint(String)` | Custom endpoint URL. **Required for Ceph S3** / S3-compatible stores. | +| `.build()` | Builds and returns an immutable `StorageConfig` instance. | + +### 4.2 AuthType Values + +| AuthType | Description | Supported CSPs | +|----------|-------------|---------------| +| `ACCESS_KEY` | Static credentials (key + secret). Use for local development and legacy environments. | All CSPs | +| `OIDC` | Workload Identity / Web Identity Token. **Preferred for Kubernetes deployments.** | All CSPs | +| `IAM` | Managed Identity / Application Default Credentials. For VMs and managed services. | AWS, Azure, GCP | +| `IAM_ROLE` | AWS STS assumed role. | AWS only | +| `INSTANCE_PROFILE` | EC2 instance profile or OCI Instance Principal. | AWS, OCI | + +### 4.3 Configuration Examples + +#### OIDC (Production — Kubernetes Workload Identity) + +```java +// AWS — OIDC via Web Identity Token (EKS IRSA) +StorageConfig awsConfig = StorageConfig.builder(StorageConfig.StorageType.AWS) + .authType(StorageConfig.AuthType.OIDC) + .region("us-east-1") + .build(); + +// Azure — Workload Identity (AKS) +// Env vars required: AZURE_CLIENT_ID, AZURE_TENANT_ID, AZURE_FEDERATED_TOKEN_FILE +StorageConfig azureConfig = StorageConfig.builder(StorageConfig.StorageType.AZURE) + .authType(StorageConfig.AuthType.OIDC) + .storageKey("my-storage-account") // account name + .region("eastus") + .build(); + +// GCP — Application Default Credentials (GKE Workload Identity) +StorageConfig gcpConfig = StorageConfig.builder(StorageConfig.StorageType.GCLOUD) + .authType(StorageConfig.AuthType.OIDC) + .region("us-central1") + .build(); + +// OCI — Instance Principal (OKE) +StorageConfig ociConfig = StorageConfig.builder(StorageConfig.StorageType.OCI) + .authType(StorageConfig.AuthType.INSTANCE_PROFILE) + .region("us-ashburn-1") + .build(); +``` + +#### ACCESS_KEY (Development / Legacy) + +```java +// AWS — Static credentials +StorageConfig awsConfig = StorageConfig.builder(StorageConfig.StorageType.AWS) + .authType(StorageConfig.AuthType.ACCESS_KEY) + .storageKey(System.getenv("AWS_ACCESS_KEY_ID")) + .storageSecret(System.getenv("AWS_SECRET_ACCESS_KEY")) + .region("us-east-1") + .build(); + +// Ceph S3-compatible (uses AWS module with endpoint override) +StorageConfig cephConfig = StorageConfig.builder(StorageConfig.StorageType.CEPHS3) + .authType(StorageConfig.AuthType.ACCESS_KEY) + .endPoint("https://ceph-rgw.internal.example.com") + .storageKey(System.getenv("CEPH_ACCESS_KEY")) + .storageSecret(System.getenv("CEPH_SECRET_KEY")) + .region("default") + .build(); + +// Azure — Shared Key +StorageConfig azureConfig = StorageConfig.builder(StorageConfig.StorageType.AZURE) + .authType(StorageConfig.AuthType.ACCESS_KEY) + .storageKey("my-storage-account") // account name + .storageSecret(System.getenv("AZURE_ACCOUNT_KEY")) // account key + .region("eastus") + .build(); + +// GCP — Service Account JSON +StorageConfig gcpConfig = StorageConfig.builder(StorageConfig.StorageType.GCLOUD) + .authType(StorageConfig.AuthType.ACCESS_KEY) + .storageSecret(System.getenv("GOOGLE_SA_JSON")) // full JSON string + .build(); + +// OCI — API Key (ACCESS_KEY) +// Also requires OCI_TENANCY_ID and OCI_FINGERPRINT env vars +StorageConfig ociConfig = StorageConfig.builder(StorageConfig.StorageType.OCI) + .authType(StorageConfig.AuthType.ACCESS_KEY) + .storageKey(System.getenv("OCI_USER_OCID")) // user OCID + .storageSecret(System.getenv("OCI_PRIVATE_KEY_PEM")) // private key PEM + .region("us-ashburn-1") + .build(); +``` + +### 4.4 Spring Boot — application.yml Integration + +**application.yml:** + +```yaml +sunbird: + storage: + type: AWS # AWS | AZURE | GCLOUD | OCI | CEPHS3 + auth-type: OIDC # OIDC | ACCESS_KEY | IAM | IAM_ROLE | INSTANCE_PROFILE + region: us-east-1 + storage-key: ${STORAGE_KEY:} # Optional — leave blank for OIDC/IAM + storage-secret: ${STORAGE_SECRET:} + endpoint: ${STORAGE_ENDPOINT:} # Required only for Ceph S3 +``` + +**StorageProperties.java:** + +```java +@Configuration +@ConfigurationProperties(prefix = "sunbird.storage") +public class StorageProperties { + private String type; + private String authType; + private String region; + private String storageKey; + private String storageSecret; + private String endpoint; + // getters + setters ... +} +``` + +**StorageServiceConfig.java:** + +```java +@Configuration +public class StorageServiceConfig { + + @Bean + @Singleton + public IStorageService storageService(StorageProperties props) { + StorageConfig.Builder builder = StorageConfig + .builder(StorageConfig.StorageType.valueOf(props.getType())) + .authType(StorageConfig.AuthType.valueOf(props.getAuthType())); + + if (props.getRegion() != null) builder.region(props.getRegion()); + if (props.getStorageKey() != null) builder.storageKey(props.getStorageKey()); + if (props.getStorageSecret() != null) builder.storageSecret(props.getStorageSecret()); + if (props.getEndpoint() != null) builder.endPoint(props.getEndpoint()); + + return StorageServiceFactory.getStorageService(builder.build()); + } +} +``` + +### 4.5 Play Framework — application.conf Integration + +**application.conf:** + +```hocon +sunbird.storage { + type = "AWS" + auth-type = "OIDC" + region = "us-east-1" + storage-key = ${?STORAGE_KEY} + storage-secret = ${?STORAGE_SECRET} + endpoint = ${?STORAGE_ENDPOINT} +} +``` + +**StorageModule.java (Guice Module):** + +```java +public class StorageModule extends AbstractModule { + + private final Configuration config; + + public StorageModule(Environment env, Configuration config) { + this.config = config; + } + + @Override + protected void configure() { + String type = config.getString("sunbird.storage.type"); + String authType = config.getString("sunbird.storage.auth-type"); + String region = config.getString("sunbird.storage.region"); + String key = config.getString("sunbird.storage.storage-key"); + String secret = config.getString("sunbird.storage.storage-secret"); + String endpoint = config.getString("sunbird.storage.endpoint"); + + StorageConfig.Builder builder = StorageConfig + .builder(StorageConfig.StorageType.valueOf(type)) + .authType(StorageConfig.AuthType.valueOf(authType)); + + if (region != null) builder.region(region); + if (key != null) builder.storageKey(key); + if (secret != null) builder.storageSecret(secret); + if (endpoint != null) builder.endPoint(endpoint); + + IStorageService service = StorageServiceFactory.getStorageService(builder.build()); + bind(IStorageService.class).toInstance(service); + } +} +``` + +Register the module in `application.conf`: + +```hocon +play.modules.enabled += "modules.StorageModule" +``` + +--- + +## 5. Usage Examples + +All examples use the `IStorageService` interface. The same code works regardless of which CSP module is on the runtime classpath. + +### 5.1 Instantiation via ServiceLoader + +```java +// Factory auto-discovers the runtime CSP module via ServiceLoader +try (IStorageService service = StorageServiceFactory.getStorageService(config)) { + + // All operations here... + +} // close() called automatically — shuts down the underlying SDK client + +// Direct instantiation (bypasses ServiceLoader; only when one CSP is on classpath) +IStorageService service = new AwsStorageService(config); +``` + +> `IStorageService extends AutoCloseable` — always use try-with-resources to ensure the underlying SDK client is properly shut down. + +### 5.2 Upload Operations + +```java +// Upload a single file (with retry support) +String objectUrl = service.upload( + "my-bucket", // container / bucket name + "/local/path/file.csv",// local file path + "data/2026/file.csv", // remote object key + false, // not a directory + 1, // attempt number (start at 1) + 3, // max retries + null // TTL in seconds (null = no expiry metadata) +); + +// Convenience overload — no retries, no TTL +String objectUrl = service.upload("my-bucket", "/local/file.csv", "data/file.csv"); + +// Upload a directory recursively (async) +CompletableFuture> future = service.uploadFolder( + "my-bucket", + "/local/export-folder", + "exports/2026-02/", + false, // not public + null, // no TTL + 2, // retries + 1 // attempt +); +List uploadedKeys = future.get(); // block for result + +// Upload raw bytes +byte[] jsonBytes = objectMapper.writeValueAsBytes(payload); +service.put("my-bucket", jsonBytes, "events/event-001.json"); +``` + +### 5.3 Download Operations + +```java +// Download a single object +service.download("my-bucket", "data/2026/file.csv", "/tmp/downloads/"); + +// Download a directory recursively +service.download("my-bucket", "exports/2026-02/", "/tmp/export/", true); + +// Read object content as text lines +List lines = service.getObjectData("my-bucket", "config/settings.json"); + +// Fetch blob metadata (without payload) +Blob blob = service.getObject("my-bucket", "data/2026/file.csv"); +System.out.println("Size: " + blob.getContentLength()); +System.out.println("Modified: " + blob.getLastModified()); + +// Fetch blob metadata with content bytes +Blob blobWithPayload = service.getObject("my-bucket", "small-file.txt", true); +String content = new String(blobWithPayload.getPayload(), StandardCharsets.UTF_8); +``` + +### 5.4 Signed URLs + +```java +// Signed read URL — valid for 1 hour +String getUrl = service.getSignedURL( + "my-bucket", + "reports/annual-report.pdf", + 3600, // TTL in seconds + "r" // permission: "r" = read, "w" = write +); + +// Signed upload URL with content type (e.g. for direct browser upload) +String putUrl = service.getSignedURLV2( + "my-bucket", + "uploads/user-avatar.png", + 900, // 15-minute TTL + "w", // write permission + "image/png", // content type + Map.of("x-amz-acl", "private") // additional params (CSP-specific) +); +``` + +### 5.5 List and Search Operations + +```java +// List all object keys under a prefix +List keys = service.listObjectKeys("my-bucket", "data/2026/"); + +// List blobs with metadata +List blobs = service.listObjects("my-bucket", "data/2026/"); + +// Date-based search — finds objects in date-prefixed paths (e.g. telemetry/raw/2026-02-14/) +List recent = service.searchObjects( + "my-bucket", + "telemetry/raw/", + null, // fromDate — null means compute from toDate minus delta + "2026-02-20", // toDate + 7, // delta: go back 7 days + "yyyy-MM-dd" // date pattern used in path segments +); + +// Search returning only keys +List recentKeys = service.searchObjectKeys( + "my-bucket", "telemetry/raw/", null, "2026-02-20", 7, "yyyy-MM-dd" +); +``` + +### 5.6 Delete Operations + +```java +// Delete a single file +service.deleteObject("my-bucket", "data/old-file.csv"); + +// Delete a directory and all its contents +service.deleteObject("my-bucket", "exports/old-export/", true); + +// Delete multiple typed targets (mix of files and directories) +service.deleteObjects("my-bucket", List.of( + DeleteTarget.file("data/file-a.csv"), + DeleteTarget.file("data/file-b.csv"), + DeleteTarget.directory("temp/staging/") +)); +``` + +### 5.7 Copy and Archive Operations + +```java +// Copy within the same bucket +service.copyObjects("my-bucket", "src/file.csv", "my-bucket", "dst/file.csv"); + +// Cross-bucket copy of an entire directory +service.copyObjects( + "source-bucket", "archive/2025/", + "dest-bucket", "archive/2025/", + true // isDirectory +); + +// Extract a ZIP archive from cloud storage to a new cloud prefix +// Downloads the ZIP, extracts locally, then uploads all files to the destination key +service.extractArchive( + "my-bucket", + "zips/dataset.zip", // source ZIP object key + "datasets/extracted/" // destination prefix in the same bucket +); +``` + +--- + +## 6. Dockerfile — Multi-Stage Builds per CSP + +All Dockerfiles below use a **multi-stage build**: + +- **Stage 1 (`builder`):** Compiles the application using a full JDK + Maven/SBT image. +- **Stage 2 (runtime):** Contains only a JRE, the assembled JAR, and no build tools. + +This pattern minimises final image size, reduces the CVE attack surface, and prevents build credentials or source code from reaching production. + +### 6.1 AWS S3 Dockerfile + +For AWS deployments, the SDK reads credentials from the pod's OIDC token (EKS IRSA) or instance profile automatically. No credentials are baked into the image. + +```dockerfile +# ── Stage 1: Build ───────────────────────────────────────────────────────────── +FROM maven:3.9-eclipse-temurin-11 AS builder + +WORKDIR /build + +# Cache Maven dependencies before copying source (faster rebuilds) +COPY pom.xml . +RUN mvn dependency:go-offline -B + +COPY src ./src + +# Build fat-JAR; integration tests are skipped (they require real cloud access) +RUN mvn clean package -DskipTests -B + + +# ── Stage 2: Runtime ─────────────────────────────────────────────────────────── +FROM eclipse-temurin:11-jre-jammy + +# Security: run as non-root +RUN groupadd -r appuser && useradd -r -g appuser appuser + +WORKDIR /app + +COPY --from=builder /build/target/*.jar app.jar + +USER appuser + +# AWS SDK reads credentials from environment / IRSA token automatically. +# Supply these at runtime via your Kubernetes ServiceAccount annotation, +# ECS task role, or EC2 instance profile — do NOT hardcode them here. +# +# Required env vars (set via K8s secret or pod spec): +# AWS_REGION — e.g. us-east-1 +# AWS_ROLE_ARN — for IRSA (EKS workload identity) +# AWS_WEB_IDENTITY_TOKEN_FILE — injected automatically by EKS + +EXPOSE 9000 + +ENTRYPOINT ["java", "-jar", "/app/app.jar"] +``` + +### 6.2 Azure Blob Storage Dockerfile + +For AKS deployments, Azure Workload Identity injects `AZURE_CLIENT_ID`, `AZURE_TENANT_ID`, and the federated token file automatically via the pod's service account. No credentials are baked into the image. + +```dockerfile +# ── Stage 1: Build ───────────────────────────────────────────────────────────── +FROM maven:3.9-eclipse-temurin-11 AS builder + +WORKDIR /build + +COPY pom.xml . +RUN mvn dependency:go-offline -B + +COPY src ./src +RUN mvn clean package -DskipTests -B + + +# ── Stage 2: Runtime ─────────────────────────────────────────────────────────── +FROM eclipse-temurin:11-jre-jammy + +RUN groupadd -r appuser && useradd -r -g appuser appuser + +WORKDIR /app + +COPY --from=builder /build/target/*.jar app.jar + +USER appuser + +# Azure Workload Identity injects these automatically on AKS: +# AZURE_CLIENT_ID +# AZURE_TENANT_ID +# AZURE_FEDERATED_TOKEN_FILE +# +# For ACCESS_KEY auth, inject AZURE_ACCOUNT_KEY via a K8s Secret. +# +# StorageConfig should be set with: +# .authType(StorageConfig.AuthType.OIDC) +# .storageKey("") + +EXPOSE 9000 + +ENTRYPOINT ["java", "-jar", "/app/app.jar"] +``` + +### 6.3 Google Cloud Storage Dockerfile + +On GKE with Workload Identity, the GCP SDK reads credentials from the node metadata server automatically — no environment variables are needed when using `AuthType.OIDC` or `IAM`. + +```dockerfile +# ── Stage 1: Build ───────────────────────────────────────────────────────────── +FROM maven:3.9-eclipse-temurin-11 AS builder + +WORKDIR /build + +COPY pom.xml . +RUN mvn dependency:go-offline -B + +COPY src ./src +RUN mvn clean package -DskipTests -B + + +# ── Stage 2: Runtime ─────────────────────────────────────────────────────────── +FROM eclipse-temurin:11-jre-jammy + +RUN groupadd -r appuser && useradd -r -g appuser appuser + +WORKDIR /app + +COPY --from=builder /build/target/*.jar app.jar + +USER appuser + +# GKE Workload Identity: no env vars required. +# Application Default Credentials (ADC) are automatically resolved via +# the GKE metadata server when the pod's KSA is annotated with a GSA. +# +# For service-account key auth (dev only), mount the JSON key file +# as a K8s Secret Volume and set: +# GOOGLE_APPLICATION_CREDENTIALS=/secrets/sa-key.json +# Never bake service account keys into the image. + +EXPOSE 9000 + +ENTRYPOINT ["java", "-jar", "/app/app.jar"] +``` + +### 6.4 Oracle Cloud (OCI) Dockerfile + +OCI deployments on OKE use Instance Principals (`INSTANCE_PROFILE` AuthType). The OCI SDK communicates with the instance identity service automatically. `OCI_TENANCY_ID` and `OCI_FINGERPRINT` are only required for `ACCESS_KEY` auth. + +```dockerfile +# ── Stage 1: Build ───────────────────────────────────────────────────────────── +FROM maven:3.9-eclipse-temurin-11 AS builder + +WORKDIR /build + +COPY pom.xml . +RUN mvn dependency:go-offline -B + +COPY src ./src +RUN mvn clean package -DskipTests -B + + +# ── Stage 2: Runtime ─────────────────────────────────────────────────────────── +FROM eclipse-temurin:11-jre-jammy + +RUN groupadd -r appuser && useradd -r -g appuser appuser + +WORKDIR /app + +COPY --from=builder /build/target/*.jar app.jar + +USER appuser + +# OKE Instance Principals: the OCI SDK automatically contacts the +# Instance Metadata Service (IMDS) on the node — no env vars needed. +# +# For ACCESS_KEY auth only (supply via K8s Secret): +# OCI_TENANCY_ID — tenancy OCID +# OCI_FINGERPRINT — API key fingerprint +# (storageKey and storageSecret come from StorageConfig) + +EXPOSE 9000 + +ENTRYPOINT ["java", "-jar", "/app/app.jar"] +``` + +### 6.5 Play Framework Dockerfile (Any CSP) + +Play 2 applications are packaged as a distribution directory (`sbt dist`) rather than a single fat JAR. The multi-stage build uses SBT to produce the `dist` package, then copies only the unpacked distribution into the JRE image. + +```dockerfile +# ── Stage 1: Build with SBT ──────────────────────────────────────────────────── +FROM sbtscala/scala-sbt:eclipse-temurin-11.0.21_9_1.9.8_2.13.12 AS builder + +WORKDIR /build + +# Cache SBT dependencies before copying source +COPY build.sbt . +COPY project ./project +RUN sbt update + +COPY . . + +# Build universal distribution ZIP +RUN sbt clean dist + +# Unzip the distribution +RUN cd target/universal \ + && unzip *.zip \ + && mv $(ls -d */ | head -1) /app-dist + + +# ── Stage 2: Runtime ─────────────────────────────────────────────────────────── +FROM eclipse-temurin:11-jre-jammy + +RUN groupadd -r appuser && useradd -r -g appuser appuser + +COPY --from=builder /app-dist /app +RUN chmod +x /app/bin/* + +USER appuser + +# Play application port +EXPOSE 9000 + +# APPLICATION_SECRET must be supplied at runtime via a K8s Secret. +# Never hardcode it here. +# -DAPPLICATION_SECRET=$(cat /run/secrets/play-secret) +ENTRYPOINT ["/app/bin/my-play-app", "-Dhttp.port=9000"] +``` + +--- + +## 7. Recommended Docker Image Strategy + +When containerising applications that use the Sunbird Cloud Storage SDK, there are two common approaches. + +### 7.1 Option A — Build-Time CSP Selection (Recommended) + +Use a Maven profile and Docker `--build-arg` to select the CSP at image build time. Each CI pipeline job produces a tagged image for its target cloud. + +**Dockerfile.dynamic:** + +```dockerfile +ARG CSP=aws + +# ── Stage 1: Build ───────────────────────────────────────────────────────────── +FROM maven:3.9-eclipse-temurin-11 AS builder +ARG CSP + +WORKDIR /build +COPY pom.xml . +RUN mvn dependency:go-offline -B -P${CSP} + +COPY src ./src +RUN mvn clean package -DskipTests -B -P${CSP} + +# ── Stage 2: Runtime ─────────────────────────────────────────────────────────── +FROM eclipse-temurin:11-jre-jammy +ARG CSP + +RUN groupadd -r appuser && useradd -r -g appuser appuser +WORKDIR /app +COPY --from=builder /build/target/*.jar app.jar + +LABEL org.sunbird.csp=${CSP} + +USER appuser +EXPOSE 9000 +ENTRYPOINT ["java", "-jar", "/app/app.jar"] +``` + +**Build commands:** + +```bash +docker build --build-arg CSP=aws -t myapp:2.0-aws . +docker build --build-arg CSP=azure -t myapp:2.0-azure . +docker build --build-arg CSP=gcp -t myapp:2.0-gcp . +docker build --build-arg CSP=oci -t myapp:2.0-oci . +``` + +**Maven profiles in pom.xml:** + +```xml + + + aws + true + + + org.sunbird + cloud-storage-sdk-aws + 2.0.0 + runtime + + + + + azure + + + org.sunbird + cloud-storage-sdk-azure + 2.0.0 + runtime + + + + + gcp + + + org.sunbird + cloud-storage-sdk-gcp + 2.0.0 + runtime + + + + + oci + + + org.sunbird + cloud-storage-sdk-oci + 2.0.0 + runtime + + + + +``` + +### 7.2 Option B — Single Image with All CSP Modules + +Include all four CSP modules in the image. The active provider is selected at runtime via `StorageConfig`. This maximises deployment flexibility but increases image size due to multiple large SDK JARs (~300–400 MB additional). + +```xml + + + + org.sunbird + cloud-storage-sdk-api + 2.0.0 + + + org.sunbird + cloud-storage-sdk-aws + 2.0.0 + runtime + + + org.sunbird + cloud-storage-sdk-azure + 2.0.0 + runtime + + + org.sunbird + cloud-storage-sdk-gcp + 2.0.0 + runtime + + + org.sunbird + cloud-storage-sdk-oci + 2.0.0 + runtime + + +``` + +### 7.3 Comparison and Recommendation + +| Criterion | Option A (CSP-tagged images) | Option B (All CSPs in one image) | +|-----------|------------------------------|----------------------------------| +| **Image size** | Lean — only the required CSP JARs are included | Larger — carries 4× the SDK JARs (~300–400 MB extra) | +| **Security surface** | Minimal — unused CSP SDKs and their CVEs are excluded | Broader — unused dependencies still present | +| **Deployment clarity** | Explicit CSP in image tag (`myapp:2.0-aws`) reduces misconfiguration | CSP determined at runtime via env var | +| **Multi-cloud flexibility** | Requires a rebuild to switch CSP | Switch CSP by changing an env var without a rebuild | +| **CI complexity** | Separate build job per CSP | Single build job | +| **Recommended for** | Single-cloud deployments (most enterprise use cases) | Multi-tenant SaaS where tenants use different CSPs | + +> **Recommendation:** Use **Option A** (Maven profiles + CSP-tagged images) for the vast majority of deployments. Option B is only warranted when your application must serve multiple CSPs from the same running instance — for example, a multi-tenant platform where each tenant brings their own cloud account. For all single-cloud deployments, Option A produces smaller, more auditable, and more secure images. + +--- + +## 8. Runtime Environment Variables + +Set these via Kubernetes Secrets, pod annotations, or ECS task definitions. **Never hardcode credentials in Dockerfiles or configuration files.** + +### AWS + +| Variable | Description | When Required | +|----------|-------------|--------------| +| `AWS_REGION` | Cloud region, e.g. `us-east-1`. Also accepted as `AWS_DEFAULT_REGION`. | Always | +| `AWS_ACCESS_KEY_ID` | Static access key ID. | `ACCESS_KEY` only | +| `AWS_SECRET_ACCESS_KEY` | Static secret access key. | `ACCESS_KEY` only | +| `AWS_ROLE_ARN` | IAM role ARN for EKS IRSA. | `OIDC` (EKS) | +| `AWS_WEB_IDENTITY_TOKEN_FILE` | Path to OIDC token file. Injected automatically by EKS. | `OIDC` (EKS) | + +### Azure + +| Variable | Description | When Required | +|----------|-------------|--------------| +| `AZURE_CLIENT_ID` | Managed identity or app registration client ID. | `OIDC` / `IAM` | +| `AZURE_TENANT_ID` | Azure Active Directory tenant ID. | `OIDC` / `IAM` | +| `AZURE_FEDERATED_TOKEN_FILE` | Path to K8s service account token. Injected by AKS Workload Identity. | `OIDC` (AKS) | +| `AZURE_ACCOUNT_KEY` | Storage account access key. Read this into `storageSecret` in your `StorageModule`. | `ACCESS_KEY` only | + +### GCP + +| Variable | Description | When Required | +|----------|-------------|--------------| +| `GOOGLE_APPLICATION_CREDENTIALS` | Path to a service account JSON key file. For dev/legacy use only. | `ACCESS_KEY` only | +| *(none for GKE)* | GKE Workload Identity binds a KSA to a GSA via pod annotation. No env vars required. | `OIDC` (GKE) | + +### OCI + +| Variable | Description | When Required | +|----------|-------------|--------------| +| `OCI_TENANCY_ID` | Tenancy OCID. | `ACCESS_KEY` only | +| `OCI_FINGERPRINT` | API key fingerprint. | `ACCESS_KEY` only | +| *(none for OKE)* | OKE Instance Principal auth uses the node's IMDS endpoint automatically. | `INSTANCE_PROFILE` | + +--- + +## 9. Exception Handling + +All `IStorageService` methods throw `StorageServiceException`, an unchecked `RuntimeException`. Wrap calls appropriately in your service layer to distinguish transient from permanent failures. + +```java +import org.sunbird.cloud.storage.exception.StorageServiceException; + +@Service +public class ContentStorageService { + + private final IStorageService storage; + + public ContentStorageService(IStorageService storage) { + this.storage = storage; + } + + public String uploadContent(String bucket, Path localFile, String remoteKey) { + try { + return storage.upload( + bucket, + localFile.toString(), + remoteKey, + false, // not a directory + 1, // first attempt + 3, // retry up to 3 times total + null // no TTL + ); + } catch (StorageServiceException e) { + log.error("Upload failed for key={}", remoteKey, e); + throw new ContentServiceException("Storage upload failed", e); + } + } +} +``` + +> **Built-in Retry:** `AbstractStorageService` provides automatic retry orchestration for `upload()` and `put()` operations. The `attempt` parameter tracks the current attempt (start at `1`) and `retryCount` sets the maximum. You do not need to implement your own retry loop. + +--- + +## 10. Quick Reference + +### CSP Comparison + +| CSP | Module Artifact | SDK Version | Recommended AuthType | HDFS Prefix Format | +|-----|----------------|-------------|---------------------|--------------------| +| AWS S3 | `cloud-storage-sdk-aws` | AWS SDK v2 `2.25.0` | `OIDC` (IRSA) | `s3n:///` | +| Azure Blob | `cloud-storage-sdk-azure` | Azure SDK `12.25.0` | `OIDC` (Workload Identity) | `wasb://@.blob.core.windows.net/` | +| GCP Storage | `cloud-storage-sdk-gcp` | Google Cloud Java `2.30.0` | `OIDC` (GKE WI) | `gs:///` | +| OCI Object Storage | `cloud-storage-sdk-oci` | OCI Java SDK `3.30.0` | `INSTANCE_PROFILE` | `oci://@/` | +| Ceph S3 | `cloud-storage-sdk-aws` | AWS SDK v2 `2.25.0` (via `endpointOverride`) | `ACCESS_KEY` | `s3n:///` | + +### Key Dependency Versions + +| Component | Version | +|-----------|---------| +| SDK | `2.0.0` | +| Java target | `11` | +| AWS SDK v2 | `2.25.0` | +| Azure Blob Storage | `12.25.0` | +| Azure Identity | `1.11.0` | +| GCP Cloud Storage | `2.30.0` | +| OCI Java SDK | `3.30.0` | +| Apache Tika | `2.9.0` | +| JUnit 5 | `5.10.1` | +| Mockito | `5.8.0` | + +### Build Commands + +```bash +# Full build with unit tests (integration tests auto-skip without env vars) +mvn clean verify + +# Build without tests +mvn clean package -DskipTests + +# Build with specific CSP profile +mvn clean package -DskipTests -Paws +mvn clean package -DskipTests -Pazure +mvn clean package -DskipTests -Pgcp +mvn clean package -DskipTests -Poci + +# Build a single module +mvn -pl cloud-storage-sdk-aws package -DskipTests + +# Run unit tests only (no cloud access needed) +mvn -pl cloud-storage-sdk-api test + +# Docker — build one image per CSP (Option A) +docker build --build-arg CSP=aws -t myapp:2.0-aws . +docker build --build-arg CSP=azure -t myapp:2.0-azure . +docker build --build-arg CSP=gcp -t myapp:2.0-gcp . +docker build --build-arg CSP=oci -t myapp:2.0-oci . +``` + +--- + +*Sunbird Cloud Storage SDK v2.0.0 — Generated February 2026* From c1bc330deb39071e905eb12ef9a445113923600b Mon Sep 17 00:00:00 2001 From: Aiman Sharief Date: Fri, 6 Mar 2026 12:13:23 +0530 Subject: [PATCH 36/47] fix: Decoding blob url since azure sdk's BlobClient.getBlobUrl() percent-encodes the blob name --- .../service/azure/AzureStorageService.java | 52 +++++++++++++------ 1 file changed, 37 insertions(+), 15 deletions(-) diff --git a/cloud-storage-sdk-azure/src/main/java/org/sunbird/cloud/storage/service/azure/AzureStorageService.java b/cloud-storage-sdk-azure/src/main/java/org/sunbird/cloud/storage/service/azure/AzureStorageService.java index 3838f9c..8614918 100644 --- a/cloud-storage-sdk-azure/src/main/java/org/sunbird/cloud/storage/service/azure/AzureStorageService.java +++ b/cloud-storage-sdk-azure/src/main/java/org/sunbird/cloud/storage/service/azure/AzureStorageService.java @@ -7,6 +7,7 @@ import com.azure.storage.blob.BlobContainerClient; import com.azure.storage.blob.BlobServiceClient; import com.azure.storage.blob.BlobServiceClientBuilder; +import com.azure.storage.blob.models.BlobHttpHeaders; import com.azure.storage.blob.models.BlobItem; import com.azure.storage.blob.models.BlobListDetails; import com.azure.storage.blob.models.BlobProperties; @@ -94,6 +95,21 @@ private BlobClient getBlobClient(String container, String objectKey) { return getContainerClient(container).getBlobClient(objectKey); } + /** + * The Azure SDK's getBlobUrl() encodes '/' in blob names as '%2F'. + * This replaces only %2F with '/', leaving all other percent-encoded characters + */ + private String decodeBlobUrl(String url) { + try { + java.net.URI uri = new java.net.URI(url); + String path = uri.getRawPath().replace("%2F", "/").replace("%2f", "/"); + String result = uri.getScheme() + "://" + uri.getRawAuthority() + path; + return uri.getRawQuery() != null ? result + "?" + uri.getRawQuery() : result; + } catch (java.net.URISyntaxException e) { + return url; + } + } + @Override protected void ensureContainerExists(String container) { try { @@ -113,10 +129,10 @@ protected String putObject(String container, String objectKey, File file) { try { BlobClient blobClient = getBlobClient(container, objectKey); String contentType = tika.detect(file); - blobClient.uploadFromFile(file.getAbsolutePath(), true); - blobClient.setHttpHeaders(new com.azure.storage.blob.models.BlobHttpHeaders() - .setContentType(contentType)); - return blobClient.getBlobUrl(); + blobClient.uploadFromFile(file.getAbsolutePath(), null, + new BlobHttpHeaders().setContentType(contentType), + null, null, null, null); + return decodeBlobUrl(blobClient.getBlobUrl()); } catch (Exception e) { throw new StorageServiceException( "Failed to put object from file: " + objectKey + " - " + e.getMessage(), e); @@ -127,8 +143,13 @@ protected String putObject(String container, String objectKey, File file) { protected String putObject(String container, String objectKey, byte[] content) { try { BlobClient blobClient = getBlobClient(container, objectKey); - blobClient.upload(new ByteArrayInputStream(content), content.length, true); - return blobClient.getBlobUrl(); + String contentType = tika.detect(new ByteArrayInputStream(content), objectKey); + blobClient.uploadWithResponse( + new com.azure.storage.blob.options.BlobParallelUploadOptions( + new ByteArrayInputStream(content), content.length) + .setHeaders(new BlobHttpHeaders().setContentType(contentType)), + null, null); + return decodeBlobUrl(blobClient.getBlobUrl()); } catch (Exception e) { throw new StorageServiceException( "Failed to put object from bytes: " + objectKey + " - " + e.getMessage(), e); @@ -148,7 +169,7 @@ protected InputStream getObjectStream(String container, String objectKey) { @Override protected String getObjectUri(String container, String objectKey) { - return getBlobClient(container, objectKey).getBlobUrl(); + return decodeBlobUrl(getBlobClient(container, objectKey).getBlobUrl()); } @Override @@ -161,7 +182,8 @@ protected BlobDetail getObjectDetail(String container, String objectKey) { if (props.getMetadata() != null) { metadata.putAll(props.getMetadata()); } - metadata.put("uri", blobClient.getBlobUrl()); + String blobUrl = decodeBlobUrl(blobClient.getBlobUrl()); + metadata.put("uri", blobUrl); metadata.put("Content-Type", props.getContentType()); metadata.put("ETag", props.getETag()); @@ -170,7 +192,7 @@ protected BlobDetail getObjectDetail(String container, String objectKey) { : null; return new BlobDetail(objectKey, props.getBlobSize(), lastModified, - metadata, blobClient.getBlobUrl()); + metadata, blobUrl); } catch (Exception e) { throw new StorageServiceException( "Failed to get object detail: " + objectKey + " - " + e.getMessage(), e); @@ -231,14 +253,14 @@ protected String generateSignedGetUrl(String container, String objectKey, int tt BlobServiceSasSignatureValues values = new BlobServiceSasSignatureValues( OffsetDateTime.now().plusSeconds(ttlSeconds), permission); + String baseUrl = decodeBlobUrl(blobClient.getBlobUrl()); if (useSharedKey) { - return blobClient.getBlobUrl() + "?" + blobClient.generateSas(values); + return baseUrl + "?" + blobClient.generateSas(values); } else { var delegationKey = blobServiceClient.getUserDelegationKey( OffsetDateTime.now().minusMinutes(5), OffsetDateTime.now().plusSeconds(ttlSeconds)); - return blobClient.getBlobUrl() + "?" + - blobClient.generateUserDelegationSas(values, delegationKey); + return baseUrl + "?" + blobClient.generateUserDelegationSas(values, delegationKey); } } catch (Exception e) { throw new StorageServiceException( @@ -261,14 +283,14 @@ protected String generateSignedPutUrl(String container, String objectKey, values.setContentType(contentType); } + String baseUrl = decodeBlobUrl(blobClient.getBlobUrl()); if (useSharedKey) { - return blobClient.getBlobUrl() + "?" + blobClient.generateSas(values); + return baseUrl + "?" + blobClient.generateSas(values); } else { var delegationKey = blobServiceClient.getUserDelegationKey( OffsetDateTime.now().minusMinutes(5), OffsetDateTime.now().plusSeconds(ttlSeconds)); - return blobClient.getBlobUrl() + "?" + - blobClient.generateUserDelegationSas(values, delegationKey); + return baseUrl + "?" + blobClient.generateUserDelegationSas(values, delegationKey); } } catch (Exception e) { throw new StorageServiceException( From 19f46a7de75d9848ec83a5a4b0cb730419309851 Mon Sep 17 00:00:00 2001 From: Aiman Sharief Date: Fri, 6 Mar 2026 12:13:54 +0530 Subject: [PATCH 37/47] fix: Added fix to handle if objectKey is already ending with "/" --- .../org/sunbird/cloud/storage/AbstractStorageService.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/cloud-storage-sdk-api/src/main/java/org/sunbird/cloud/storage/AbstractStorageService.java b/cloud-storage-sdk-api/src/main/java/org/sunbird/cloud/storage/AbstractStorageService.java index 934f376..62c2687 100644 --- a/cloud-storage-sdk-api/src/main/java/org/sunbird/cloud/storage/AbstractStorageService.java +++ b/cloud-storage-sdk-api/src/main/java/org/sunbird/cloud/storage/AbstractStorageService.java @@ -106,7 +106,8 @@ public String upload(String container, String file, String objectKey, for (File f : files) { String relativePath = f.getAbsolutePath() .substring((dir.getAbsolutePath() + File.separator).length()); - String key = objectKey + "/" + relativePath; + String prefix = objectKey.endsWith("/") ? objectKey : objectKey + "/"; + String key = prefix + relativePath; urls.add(upload(container, f.getAbsolutePath(), key, false, attempt, maxAttempts, ttl)); } return String.join(",", urls); @@ -153,7 +154,8 @@ public CompletableFuture> uploadFolder(String container, String fil for (File f : files) { String relativePath = f.getAbsolutePath() .substring((dir.getAbsolutePath() + File.separator).length()); - String key = objectKey + "/" + relativePath; + String prefix = objectKey.endsWith("/") ? objectKey : objectKey + "/"; + String key = prefix + relativePath; futures.add(CompletableFuture.supplyAsync(() -> upload(container, f.getAbsolutePath(), key, false, attempt, retryCount != null ? retryCount : 0, ttl))); From d0578d6096ebfa4db5ccd46aad6ed01ec7fc0d68 Mon Sep 17 00:00:00 2001 From: Aiman Sharief Date: Fri, 6 Mar 2026 12:14:13 +0530 Subject: [PATCH 38/47] fix: Updating the versions --- cloud-storage-sdk-api/pom.xml | 2 +- cloud-storage-sdk-aws/pom.xml | 2 +- cloud-storage-sdk-azure/pom.xml | 2 +- cloud-storage-sdk-gcp/pom.xml | 2 +- cloud-storage-sdk-oci/pom.xml | 2 +- pom.xml | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/cloud-storage-sdk-api/pom.xml b/cloud-storage-sdk-api/pom.xml index 546dc3b..3dfac1d 100644 --- a/cloud-storage-sdk-api/pom.xml +++ b/cloud-storage-sdk-api/pom.xml @@ -5,7 +5,7 @@ org.sunbird cloud-storage-sdk-parent - 2.0.0 + 2.0.1 cloud-storage-sdk-api diff --git a/cloud-storage-sdk-aws/pom.xml b/cloud-storage-sdk-aws/pom.xml index ea3c22e..ef0e265 100644 --- a/cloud-storage-sdk-aws/pom.xml +++ b/cloud-storage-sdk-aws/pom.xml @@ -5,7 +5,7 @@ org.sunbird cloud-storage-sdk-parent - 2.0.0 + 2.0.1 cloud-storage-sdk-aws diff --git a/cloud-storage-sdk-azure/pom.xml b/cloud-storage-sdk-azure/pom.xml index acc0a73..1609cfb 100644 --- a/cloud-storage-sdk-azure/pom.xml +++ b/cloud-storage-sdk-azure/pom.xml @@ -5,7 +5,7 @@ org.sunbird cloud-storage-sdk-parent - 2.0.0 + 2.0.1 cloud-storage-sdk-azure diff --git a/cloud-storage-sdk-gcp/pom.xml b/cloud-storage-sdk-gcp/pom.xml index 289ba95..d9c49a2 100644 --- a/cloud-storage-sdk-gcp/pom.xml +++ b/cloud-storage-sdk-gcp/pom.xml @@ -5,7 +5,7 @@ org.sunbird cloud-storage-sdk-parent - 2.0.0 + 2.0.1 cloud-storage-sdk-gcp diff --git a/cloud-storage-sdk-oci/pom.xml b/cloud-storage-sdk-oci/pom.xml index f89d848..9094b15 100644 --- a/cloud-storage-sdk-oci/pom.xml +++ b/cloud-storage-sdk-oci/pom.xml @@ -5,7 +5,7 @@ org.sunbird cloud-storage-sdk-parent - 2.0.0 + 2.0.1 cloud-storage-sdk-oci diff --git a/pom.xml b/pom.xml index e65d873..ccec63a 100644 --- a/pom.xml +++ b/pom.xml @@ -4,7 +4,7 @@ 4.0.0 org.sunbird cloud-storage-sdk-parent - 2.0.0 + 2.0.1 pom Cloud Storage SDK Parent Multi-module cloud storage SDK providing CSP-specific implementations using official cloud SDKs. From 634066ea621d45c4e7fede48e837e906db57dff5 Mon Sep 17 00:00:00 2001 From: Aiman Sharief Date: Wed, 11 Mar 2026 13:19:13 +0530 Subject: [PATCH 39/47] fix: Using the BlobUploadFromFileOptions to maintain consistency in uploads and avoid 5 null arguments code --- .../cloud/storage/service/azure/AzureStorageService.java | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/cloud-storage-sdk-azure/src/main/java/org/sunbird/cloud/storage/service/azure/AzureStorageService.java b/cloud-storage-sdk-azure/src/main/java/org/sunbird/cloud/storage/service/azure/AzureStorageService.java index 8614918..20263c7 100644 --- a/cloud-storage-sdk-azure/src/main/java/org/sunbird/cloud/storage/service/azure/AzureStorageService.java +++ b/cloud-storage-sdk-azure/src/main/java/org/sunbird/cloud/storage/service/azure/AzureStorageService.java @@ -129,9 +129,10 @@ protected String putObject(String container, String objectKey, File file) { try { BlobClient blobClient = getBlobClient(container, objectKey); String contentType = tika.detect(file); - blobClient.uploadFromFile(file.getAbsolutePath(), null, - new BlobHttpHeaders().setContentType(contentType), - null, null, null, null); + blobClient.uploadFromFileWithResponse( + new com.azure.storage.blob.options.BlobUploadFromFileOptions(file.getAbsolutePath()) + .setHeaders(new BlobHttpHeaders().setContentType(contentType)), + null, null); return decodeBlobUrl(blobClient.getBlobUrl()); } catch (Exception e) { throw new StorageServiceException( From 919bff8be89948f366541877ea9c1b0a58a57316 Mon Sep 17 00:00:00 2001 From: Aiman Sharief Date: Wed, 11 Mar 2026 13:22:57 +0530 Subject: [PATCH 40/47] fix: Added test cases to cover the optional slash in objectKey --- .../storage/AbstractStorageServiceTest.java | 66 +++++++++++++++++++ 1 file changed, 66 insertions(+) diff --git a/cloud-storage-sdk-api/src/test/java/org/sunbird/cloud/storage/AbstractStorageServiceTest.java b/cloud-storage-sdk-api/src/test/java/org/sunbird/cloud/storage/AbstractStorageServiceTest.java index 0b95a29..9d0f3c5 100644 --- a/cloud-storage-sdk-api/src/test/java/org/sunbird/cloud/storage/AbstractStorageServiceTest.java +++ b/cloud-storage-sdk-api/src/test/java/org/sunbird/cloud/storage/AbstractStorageServiceTest.java @@ -12,6 +12,7 @@ import java.nio.file.Files; import java.nio.file.Path; import java.util.*; +import java.util.concurrent.ExecutionException; import java.util.stream.Collectors; import static org.junit.jupiter.api.Assertions.*; @@ -60,6 +61,71 @@ void upload_directory_uploadsAllFiles() throws IOException { assertTrue(service.exists("bucket", "prefix/data/b.txt")); } + @Test + void upload_directory_withTrailingSlash_noDoubleSlash() throws IOException { + Path dir = tempDir.resolve("data-slash"); + Files.createDirectory(dir); + Files.writeString(dir.resolve("a.txt"), "aaa"); + Files.writeString(dir.resolve("b.txt"), "bbb"); + + // objectKey already ends with '/' — must not produce "prefix/data//a.txt" + String result = service.upload("bucket", dir.toString(), "prefix/data/", true, 1, 0, null); + + assertTrue(result.contains("prefix/data/a.txt"), "Expected prefix/data/a.txt, got: " + result); + assertTrue(result.contains("prefix/data/b.txt"), "Expected prefix/data/b.txt, got: " + result); + assertFalse(result.contains("//"), "Keys must not contain double slashes: " + result); + assertTrue(service.exists("bucket", "prefix/data/a.txt")); + assertTrue(service.exists("bucket", "prefix/data/b.txt")); + } + + @Test + void upload_directory_withoutTrailingSlash_addsSingleSlash() throws IOException { + Path dir = tempDir.resolve("data-noslash"); + Files.createDirectory(dir); + Files.writeString(dir.resolve("c.txt"), "ccc"); + + // objectKey has no trailing '/' — must add exactly one + String result = service.upload("bucket", dir.toString(), "prefix/data", true, 1, 0, null); + + assertTrue(result.contains("prefix/data/c.txt"), "Expected prefix/data/c.txt, got: " + result); + assertFalse(result.contains("//"), "Keys must not contain double slashes: " + result); + assertTrue(service.exists("bucket", "prefix/data/c.txt")); + } + + @Test + void uploadFolder_withTrailingSlash_noDoubleSlash() throws ExecutionException, InterruptedException { + Path dir = tempDir.resolve("async-slash"); + Files.createDirectory(dir); + Files.writeString(dir.resolve("x.txt"), "xxx"); + Files.writeString(dir.resolve("y.txt"), "yyy"); + + // async uploadFolder — same prefix logic at line 157-158 + List urls = service.uploadFolder("bucket", dir.toString(), "async/data/") + .get(); + + assertTrue(urls.stream().anyMatch(u -> u.contains("async/data/x.txt")), + "Expected async/data/x.txt in: " + urls); + assertTrue(urls.stream().anyMatch(u -> u.contains("async/data/y.txt")), + "Expected async/data/y.txt in: " + urls); + urls.forEach(u -> assertFalse(u.contains("//"), + "URL must not contain double slashes: " + u)); + } + + @Test + void uploadFolder_withoutTrailingSlash_addsSingleSlash() throws ExecutionException, InterruptedException { + Path dir = tempDir.resolve("async-noslash"); + Files.createDirectory(dir); + Files.writeString(dir.resolve("z.txt"), "zzz"); + + List urls = service.uploadFolder("bucket", dir.toString(), "async/data") + .get(); + + assertTrue(urls.stream().anyMatch(u -> u.contains("async/data/z.txt")), + "Expected async/data/z.txt in: " + urls); + urls.forEach(u -> assertFalse(u.contains("//"), + "URL must not contain double slashes: " + u)); + } + @Test void upload_exceedsMaxRetries_throwsException() { // Non-existent file forces an exception, and retry count 1 means fail immediately From 1b2c241075e3af2cc52b6c6dce14f5ec3ad3b387 Mon Sep 17 00:00:00 2001 From: Aiman Sharief Date: Wed, 11 Mar 2026 13:23:53 +0530 Subject: [PATCH 41/47] fix: Added Imports to improve code readability and consistency within the file --- .../cloud/storage/service/azure/AzureStorageService.java | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/cloud-storage-sdk-azure/src/main/java/org/sunbird/cloud/storage/service/azure/AzureStorageService.java b/cloud-storage-sdk-azure/src/main/java/org/sunbird/cloud/storage/service/azure/AzureStorageService.java index 20263c7..b502076 100644 --- a/cloud-storage-sdk-azure/src/main/java/org/sunbird/cloud/storage/service/azure/AzureStorageService.java +++ b/cloud-storage-sdk-azure/src/main/java/org/sunbird/cloud/storage/service/azure/AzureStorageService.java @@ -12,6 +12,8 @@ import com.azure.storage.blob.models.BlobListDetails; import com.azure.storage.blob.models.BlobProperties; import com.azure.storage.blob.models.ListBlobsOptions; +import com.azure.storage.blob.options.BlobParallelUploadOptions; +import com.azure.storage.blob.options.BlobUploadFromFileOptions; import com.azure.storage.blob.sas.BlobSasPermission; import com.azure.storage.blob.sas.BlobServiceSasSignatureValues; import com.azure.storage.common.StorageSharedKeyCredential; @@ -130,7 +132,7 @@ protected String putObject(String container, String objectKey, File file) { BlobClient blobClient = getBlobClient(container, objectKey); String contentType = tika.detect(file); blobClient.uploadFromFileWithResponse( - new com.azure.storage.blob.options.BlobUploadFromFileOptions(file.getAbsolutePath()) + new BlobUploadFromFileOptions(file.getAbsolutePath()) .setHeaders(new BlobHttpHeaders().setContentType(contentType)), null, null); return decodeBlobUrl(blobClient.getBlobUrl()); @@ -146,8 +148,7 @@ protected String putObject(String container, String objectKey, byte[] content) { BlobClient blobClient = getBlobClient(container, objectKey); String contentType = tika.detect(new ByteArrayInputStream(content), objectKey); blobClient.uploadWithResponse( - new com.azure.storage.blob.options.BlobParallelUploadOptions( - new ByteArrayInputStream(content), content.length) + new BlobParallelUploadOptions(new ByteArrayInputStream(content), content.length) .setHeaders(new BlobHttpHeaders().setContentType(contentType)), null, null); return decodeBlobUrl(blobClient.getBlobUrl()); From ae7b37f00b1bbff4a2c4a2787f94fafa0e849343 Mon Sep 17 00:00:00 2001 From: Aiman Sharief Date: Wed, 11 Mar 2026 15:05:28 +0530 Subject: [PATCH 42/47] fix: Update test cases to cover the optional slash in objectKey --- .../storage/AbstractStorageServiceTest.java | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/cloud-storage-sdk-api/src/test/java/org/sunbird/cloud/storage/AbstractStorageServiceTest.java b/cloud-storage-sdk-api/src/test/java/org/sunbird/cloud/storage/AbstractStorageServiceTest.java index 9d0f3c5..14a0e18 100644 --- a/cloud-storage-sdk-api/src/test/java/org/sunbird/cloud/storage/AbstractStorageServiceTest.java +++ b/cloud-storage-sdk-api/src/test/java/org/sunbird/cloud/storage/AbstractStorageServiceTest.java @@ -73,7 +73,7 @@ void upload_directory_withTrailingSlash_noDoubleSlash() throws IOException { assertTrue(result.contains("prefix/data/a.txt"), "Expected prefix/data/a.txt, got: " + result); assertTrue(result.contains("prefix/data/b.txt"), "Expected prefix/data/b.txt, got: " + result); - assertFalse(result.contains("//"), "Keys must not contain double slashes: " + result); + assertFalse(pathOf(result).contains("//"), "Keys must not contain double slashes: " + result); assertTrue(service.exists("bucket", "prefix/data/a.txt")); assertTrue(service.exists("bucket", "prefix/data/b.txt")); } @@ -88,12 +88,12 @@ void upload_directory_withoutTrailingSlash_addsSingleSlash() throws IOException String result = service.upload("bucket", dir.toString(), "prefix/data", true, 1, 0, null); assertTrue(result.contains("prefix/data/c.txt"), "Expected prefix/data/c.txt, got: " + result); - assertFalse(result.contains("//"), "Keys must not contain double slashes: " + result); + assertFalse(pathOf(result).contains("//"), "Keys must not contain double slashes: " + result); assertTrue(service.exists("bucket", "prefix/data/c.txt")); } @Test - void uploadFolder_withTrailingSlash_noDoubleSlash() throws ExecutionException, InterruptedException { + void uploadFolder_withTrailingSlash_noDoubleSlash() throws ExecutionException, InterruptedException, IOException { Path dir = tempDir.resolve("async-slash"); Files.createDirectory(dir); Files.writeString(dir.resolve("x.txt"), "xxx"); @@ -107,12 +107,12 @@ void uploadFolder_withTrailingSlash_noDoubleSlash() throws ExecutionException, I "Expected async/data/x.txt in: " + urls); assertTrue(urls.stream().anyMatch(u -> u.contains("async/data/y.txt")), "Expected async/data/y.txt in: " + urls); - urls.forEach(u -> assertFalse(u.contains("//"), + urls.forEach(u -> assertFalse(pathOf(u).contains("//"), "URL must not contain double slashes: " + u)); } @Test - void uploadFolder_withoutTrailingSlash_addsSingleSlash() throws ExecutionException, InterruptedException { + void uploadFolder_withoutTrailingSlash_addsSingleSlash() throws ExecutionException, InterruptedException, IOException { Path dir = tempDir.resolve("async-noslash"); Files.createDirectory(dir); Files.writeString(dir.resolve("z.txt"), "zzz"); @@ -122,7 +122,7 @@ void uploadFolder_withoutTrailingSlash_addsSingleSlash() throws ExecutionExcepti assertTrue(urls.stream().anyMatch(u -> u.contains("async/data/z.txt")), "Expected async/data/z.txt in: " + urls); - urls.forEach(u -> assertFalse(u.contains("//"), + urls.forEach(u -> assertFalse(pathOf(u).contains("//"), "URL must not contain double slashes: " + u)); } @@ -485,4 +485,10 @@ public void close() { store.clear(); } } + + /** Removes all scheme separators ("://") so that double-slash checks only apply + * to path segments, not the scheme — handles both single and multi-URL strings. */ + private static String pathOf(String url) { + return url.replace("://", ""); + } } From e20e45bb046ace7aa17dda9bedebb9aba929158e Mon Sep 17 00:00:00 2001 From: Mahesh Kumar Gangula Date: Fri, 13 Mar 2026 14:55:01 +0530 Subject: [PATCH 43/47] Storage SDK: streaming, security and fixes Multiple improvements across storage SDKs: add try-with-resources for object streams and enforce a 100MB in-memory payload limit; make archive extraction use a configurable local_extract_path and clean up extraction directory after use. Fix zip slip vulnerability in FileUtil. Harden Blob by defensively copying payload and lastModified and add equals/hashCode; add equals/hashCode for DeleteTarget. AWS: apply S3 service configuration to presigner and detect content type using tika. Azure: decode source blob URL when copying and clarify client close behavior. GCP: switch to streaming upload/download channels, cache signing Storage when created from additional params, and add a warning when using inline service account JSON. OCI: initialize client more safely with cleanup on failure, read file bytes to compute MD5 and avoid raw FileInputStream streaming, and use Instant for expiry computation. Miscellaneous logging and minor robustness fixes. --- .../cloud/storage/AbstractStorageService.java | 48 ++++++++++++------- .../org/sunbird/cloud/storage/model/Blob.java | 21 ++++++-- .../cloud/storage/model/DeleteTarget.java | 13 +++++ .../sunbird/cloud/storage/util/FileUtil.java | 5 ++ .../service/aws/AwsStorageService.java | 3 +- .../service/azure/AzureStorageService.java | 5 +- .../service/gcp/GcpStorageService.java | 29 ++++++++--- .../service/oci/OciStorageService.java | 46 +++++++----------- 8 files changed, 112 insertions(+), 58 deletions(-) diff --git a/cloud-storage-sdk-api/src/main/java/org/sunbird/cloud/storage/AbstractStorageService.java b/cloud-storage-sdk-api/src/main/java/org/sunbird/cloud/storage/AbstractStorageService.java index 62c2687..f2d50d2 100644 --- a/cloud-storage-sdk-api/src/main/java/org/sunbird/cloud/storage/AbstractStorageService.java +++ b/cloud-storage-sdk-api/src/main/java/org/sunbird/cloud/storage/AbstractStorageService.java @@ -229,23 +229,25 @@ public void download(String container, String objectKey, String localPath, boole if (isDirectory) { List objects = listObjectKeys(container, objectKey); for (String obj : objects) { - InputStream stream = getObjectStream(container, obj); - String relativePath = obj.startsWith(objectKey) ? obj.substring(objectKey.length()) : obj; - File targetFile = new File(localPath, relativePath); - File parentDir = targetFile.getParentFile(); - if (parentDir != null && !parentDir.exists()) { - parentDir.mkdirs(); + try (InputStream stream = getObjectStream(container, obj)) { + String relativePath = obj.startsWith(objectKey) ? obj.substring(objectKey.length()) : obj; + File targetFile = new File(localPath, relativePath); + File parentDir = targetFile.getParentFile(); + if (parentDir != null && !parentDir.exists()) { + parentDir.mkdirs(); + } + String fileName = targetFile.getName(); + String dirPath = parentDir != null ? parentDir.getAbsolutePath() + "/" : localPath; + FileUtil.copyStream(stream, dirPath, fileName); } - String fileName = targetFile.getName(); - String dirPath = parentDir != null ? parentDir.getAbsolutePath() + "/" : localPath; - FileUtil.copyStream(stream, dirPath, fileName); } } else { - InputStream stream = getObjectStream(container, objectKey); - String fileName = objectKey.contains("/") - ? objectKey.substring(objectKey.lastIndexOf('/') + 1) - : objectKey; - FileUtil.copyStream(stream, localPath, fileName); + try (InputStream stream = getObjectStream(container, objectKey)) { + String fileName = objectKey.contains("/") + ? objectKey.substring(objectKey.lastIndexOf('/') + 1) + : objectKey; + FileUtil.copyStream(stream, localPath, fileName); + } } } catch (StorageServiceException e) { throw e; @@ -285,6 +287,11 @@ public Blob getObject(String container, String objectKey, boolean withPayload) { BlobDetail detail = getObjectDetail(container, objectKey); byte[] payload = null; if (withPayload) { + if (detail.contentLength > 100 * 1024 * 1024) { // 100MB limit + throw new StorageServiceException( + "Object too large for in-memory payload (" + detail.contentLength + " bytes). " + + "Use getObjectStream() for large objects."); + } try (InputStream is = getObjectStream(container, objectKey)) { payload = is.readAllBytes(); } @@ -379,9 +386,9 @@ public void copyObjects(String fromContainer, String fromKey, @Override public void extractArchive(String container, String objectKey, String toKey) { + String localExtractPath = System.getProperty("local_extract_path", + System.getenv().getOrDefault("local_extract_path", "/tmp/extract")); try { - String localExtractPath = System.getProperty("local_extract_path", - System.getenv().getOrDefault("local_extract_path", "/tmp/extract")); download(container, objectKey, localExtractPath, false); String archiveName = objectKey.contains("/") ? objectKey.substring(objectKey.lastIndexOf('/') + 1) @@ -396,6 +403,15 @@ public void extractArchive(String container, String objectKey, String toKey) { throw e; } catch (Exception e) { throw new StorageServiceException("Extract archive failed: " + e.getMessage(), e); + } finally { + try { + java.nio.file.Files.walk(java.nio.file.Paths.get(localExtractPath)) + .sorted(java.util.Comparator.reverseOrder()) + .map(java.nio.file.Path::toFile) + .forEach(File::delete); + } catch (IOException cleanupEx) { + logger.warn("Failed to clean up extraction directory: {}", localExtractPath, cleanupEx); + } } } diff --git a/cloud-storage-sdk-api/src/main/java/org/sunbird/cloud/storage/model/Blob.java b/cloud-storage-sdk-api/src/main/java/org/sunbird/cloud/storage/model/Blob.java index e8b5af0..0a81ebb 100644 --- a/cloud-storage-sdk-api/src/main/java/org/sunbird/cloud/storage/model/Blob.java +++ b/cloud-storage-sdk-api/src/main/java/org/sunbird/cloud/storage/model/Blob.java @@ -19,9 +19,9 @@ public Blob(String key, long contentLength, Date lastModified, Map metadata, byte[] payload) { this.key = key; this.contentLength = contentLength; - this.lastModified = lastModified; + this.lastModified = lastModified != null ? new Date(lastModified.getTime()) : null; this.metadata = metadata != null ? Collections.unmodifiableMap(metadata) : Collections.emptyMap(); - this.payload = payload; + this.payload = payload != null ? payload.clone() : null; } public Blob(String key, long contentLength, Date lastModified, Map metadata) { @@ -37,7 +37,7 @@ public long getContentLength() { } public Date getLastModified() { - return lastModified; + return lastModified != null ? new Date(lastModified.getTime()) : null; } public Map getMetadata() { @@ -45,7 +45,20 @@ public Map getMetadata() { } public byte[] getPayload() { - return payload; + return payload != null ? payload.clone() : null; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Blob blob = (Blob) o; + return java.util.Objects.equals(key, blob.key); + } + + @Override + public int hashCode() { + return java.util.Objects.hash(key); } @Override diff --git a/cloud-storage-sdk-api/src/main/java/org/sunbird/cloud/storage/model/DeleteTarget.java b/cloud-storage-sdk-api/src/main/java/org/sunbird/cloud/storage/model/DeleteTarget.java index 496b318..5189b1f 100644 --- a/cloud-storage-sdk-api/src/main/java/org/sunbird/cloud/storage/model/DeleteTarget.java +++ b/cloud-storage-sdk-api/src/main/java/org/sunbird/cloud/storage/model/DeleteTarget.java @@ -29,6 +29,19 @@ public boolean isDirectory() { return directory; } + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + DeleteTarget that = (DeleteTarget) o; + return directory == that.directory && java.util.Objects.equals(objectKey, that.objectKey); + } + + @Override + public int hashCode() { + return java.util.Objects.hash(objectKey, directory); + } + @Override public String toString() { return "DeleteTarget{key='" + objectKey + "', directory=" + directory + "}"; diff --git a/cloud-storage-sdk-api/src/main/java/org/sunbird/cloud/storage/util/FileUtil.java b/cloud-storage-sdk-api/src/main/java/org/sunbird/cloud/storage/util/FileUtil.java index 618e92a..01eaa77 100644 --- a/cloud-storage-sdk-api/src/main/java/org/sunbird/cloud/storage/util/FileUtil.java +++ b/cloud-storage-sdk-api/src/main/java/org/sunbird/cloud/storage/util/FileUtil.java @@ -85,6 +85,11 @@ public static void unZip(String zipFile, String outputFolder) throws IOException } File newFile = new File(outputFolder + File.separator + entryName); + String canonicalDestDir = folder.getCanonicalPath(); + String canonicalNewFile = newFile.getCanonicalPath(); + if (!canonicalNewFile.startsWith(canonicalDestDir + File.separator)) { + throw new IOException("Zip entry is outside of the target dir: " + entryName); + } File parent = newFile.getParentFile(); if (parent != null && !parent.exists()) { parent.mkdirs(); diff --git a/cloud-storage-sdk-aws/src/main/java/org/sunbird/cloud/storage/service/aws/AwsStorageService.java b/cloud-storage-sdk-aws/src/main/java/org/sunbird/cloud/storage/service/aws/AwsStorageService.java index e232e01..6d9bca7 100644 --- a/cloud-storage-sdk-aws/src/main/java/org/sunbird/cloud/storage/service/aws/AwsStorageService.java +++ b/cloud-storage-sdk-aws/src/main/java/org/sunbird/cloud/storage/service/aws/AwsStorageService.java @@ -102,6 +102,7 @@ public AwsStorageService(StorageConfig config) { .pathStyleAccessEnabled(true) .build(); clientBuilder.serviceConfiguration(s3Config); + presignerBuilder.serviceConfiguration(s3Config); } this.s3Client = clientBuilder.build(); @@ -167,7 +168,7 @@ protected String putObject(String container, String objectKey, byte[] content) { PutObjectRequest request = PutObjectRequest.builder() .bucket(container) .key(objectKey) - .contentType("application/octet-stream") + .contentType(tika.detect(new java.io.ByteArrayInputStream(content), objectKey)) .build(); s3Client.putObject(request, RequestBody.fromBytes(content)); return getObjectUri(container, objectKey); diff --git a/cloud-storage-sdk-azure/src/main/java/org/sunbird/cloud/storage/service/azure/AzureStorageService.java b/cloud-storage-sdk-azure/src/main/java/org/sunbird/cloud/storage/service/azure/AzureStorageService.java index b502076..efe2a1d 100644 --- a/cloud-storage-sdk-azure/src/main/java/org/sunbird/cloud/storage/service/azure/AzureStorageService.java +++ b/cloud-storage-sdk-azure/src/main/java/org/sunbird/cloud/storage/service/azure/AzureStorageService.java @@ -239,7 +239,7 @@ protected void copyObject(String fromContainer, String fromKey, try { BlobClient sourceBlobClient = getBlobClient(fromContainer, fromKey); BlobClient destBlobClient = getBlobClient(toContainer, toKey); - destBlobClient.copyFromUrl(sourceBlobClient.getBlobUrl()); + destBlobClient.copyFromUrl(decodeBlobUrl(sourceBlobClient.getBlobUrl())); } catch (Exception e) { throw new StorageServiceException( "Failed to copy object from " + fromContainer + "/" + fromKey @@ -307,7 +307,8 @@ protected String getHdfsPrefix(String container) { @Override public void close() { - // BlobServiceClient does not implement Closeable; no resource cleanup needed + // BlobServiceClient manages its own HTTP connection pool internally + // and does not implement Closeable. No explicit cleanup is needed. logger.info("AzureStorageService closed"); } } diff --git a/cloud-storage-sdk-gcp/src/main/java/org/sunbird/cloud/storage/service/gcp/GcpStorageService.java b/cloud-storage-sdk-gcp/src/main/java/org/sunbird/cloud/storage/service/gcp/GcpStorageService.java index 8ae0956..d35ff2e 100644 --- a/cloud-storage-sdk-gcp/src/main/java/org/sunbird/cloud/storage/service/gcp/GcpStorageService.java +++ b/cloud-storage-sdk-gcp/src/main/java/org/sunbird/cloud/storage/service/gcp/GcpStorageService.java @@ -36,6 +36,7 @@ public class GcpStorageService extends AbstractStorageService { private final StorageConfig config; private final Storage storage; + private Storage signingStorage; public GcpStorageService(StorageConfig config) { this.config = config; @@ -52,6 +53,7 @@ private Storage buildStorage(StorageConfig config) { // For access key auth, storageKey is the project ID and storageSecret // is a service account JSON key. Parse as JSON credentials. if (config.getStorageSecret() != null && !config.getStorageSecret().isEmpty()) { + logger.warn("Using service account JSON from config. For production, prefer GOOGLE_APPLICATION_CREDENTIALS env var."); GoogleCredentials credentials = GoogleCredentials.fromStream( new ByteArrayInputStream(config.getStorageSecret().getBytes())); builder.setCredentials(credentials); @@ -99,7 +101,16 @@ protected String putObject(String container, String objectKey, File file) { BlobInfo blobInfo = BlobInfo.newBuilder(blobId) .setContentType(contentType) .build(); - storage.create(blobInfo, Files.readAllBytes(file.toPath())); + try (java.nio.channels.WritableByteChannel writer = storage.writer(blobInfo); + java.io.FileInputStream fis = new java.io.FileInputStream(file); + java.nio.channels.ReadableByteChannel reader = java.nio.channels.Channels.newChannel(fis)) { + java.nio.ByteBuffer buffer = java.nio.ByteBuffer.allocate(64 * 1024); + while (reader.read(buffer) > 0) { + buffer.flip(); + writer.write(buffer); + buffer.clear(); + } + } return GCS_HOST + container + "/" + objectKey; } catch (IOException e) { throw new StorageServiceException( @@ -118,8 +129,8 @@ protected String putObject(String container, String objectKey, byte[] content) { @Override protected InputStream getObjectStream(String container, String objectKey) { try { - byte[] content = storage.readAllBytes(BlobId.of(container, objectKey)); - return new ByteArrayInputStream(content); + com.google.cloud.ReadChannel reader = storage.reader(BlobId.of(container, objectKey)); + return java.nio.channels.Channels.newInputStream(reader); } catch (Exception e) { throw new StorageServiceException( "Failed to get object stream: " + objectKey + " - " + e.getMessage(), e); @@ -270,30 +281,34 @@ protected String generateSignedPutUrl(String container, String objectKey, */ private Storage resolveSigningStorage(Map additionalParams) { if (additionalParams == null || additionalParams.isEmpty()) { - return storage; + return this.storage; } String clientId = additionalParams.get("clientId"); String clientEmail = additionalParams.get("clientEmail"); String privateKeyPkcs8 = additionalParams.get("privateKeyPkcs8"); - String privateKeyId = additionalParams.get("privateKeyIds"); + String privateKeyId = additionalParams.get("privateKeyId"); String projectId = additionalParams.get("projectId"); if (clientEmail != null && privateKeyPkcs8 != null && privateKeyId != null) { + if (this.signingStorage != null) { + return this.signingStorage; + } try { ServiceAccountCredentials credentials = ServiceAccountCredentials.fromPkcs8( clientId, clientEmail, privateKeyPkcs8, privateKeyId, new ArrayList<>()); - return StorageOptions.newBuilder() + this.signingStorage = StorageOptions.newBuilder() .setProjectId(projectId) .setCredentials(credentials) .build() .getService(); + return this.signingStorage; } catch (IOException e) { throw new StorageServiceException( "Failed to create signing credentials from additionalParams: " + e.getMessage(), e); } } - return storage; + return this.storage; } @Override diff --git a/cloud-storage-sdk-oci/src/main/java/org/sunbird/cloud/storage/service/oci/OciStorageService.java b/cloud-storage-sdk-oci/src/main/java/org/sunbird/cloud/storage/service/oci/OciStorageService.java index 850363a..3461354 100644 --- a/cloud-storage-sdk-oci/src/main/java/org/sunbird/cloud/storage/service/oci/OciStorageService.java +++ b/cloud-storage-sdk-oci/src/main/java/org/sunbird/cloud/storage/service/oci/OciStorageService.java @@ -20,8 +20,8 @@ import java.io.ByteArrayInputStream; import java.io.File; -import java.io.FileInputStream; import java.io.InputStream; +import java.nio.file.Files; import java.security.MessageDigest; import java.util.ArrayList; import java.util.Base64; @@ -54,12 +54,17 @@ public OciStorageService(StorageConfig config) { clientBuilder.region(Region.fromRegionId(config.getRegion())); } - this.objectStorageClient = clientBuilder.build(authProvider); - - // Retrieve namespace - GetNamespaceResponse namespaceResponse = objectStorageClient.getNamespace( - GetNamespaceRequest.builder().build()); - this.namespace = namespaceResponse.getValue(); + ObjectStorageClient tempClient = clientBuilder.build(authProvider); + try { + GetNamespaceResponse namespaceResponse = tempClient.getNamespace( + GetNamespaceRequest.builder().build()); + this.namespace = namespaceResponse.getValue(); + this.objectStorageClient = tempClient; + } catch (Exception e) { + tempClient.close(); + throw new StorageServiceException( + "Failed to initialize OCI storage - could not get namespace: " + e.getMessage(), e); + } logger.info("Initialized OciStorageService, namespace={}, authType={}", namespace, config.getAuthType()); @@ -126,18 +131,19 @@ protected void ensureContainerExists(String container) { @Override protected String putObject(String container, String objectKey, File file) { - try (FileInputStream fis = new FileInputStream(file)) { + try { String contentType = tika.detect(file); - String md5 = computeMd5Base64(file); + byte[] data = Files.readAllBytes(file.toPath()); + String md5 = computeMd5Base64(data); PutObjectRequest request = PutObjectRequest.builder() .namespaceName(namespace) .bucketName(container) .objectName(objectKey) .contentType(contentType) - .contentLength(file.length()) + .contentLength((long) data.length) .contentMD5(md5) - .putObjectBody(fis) + .putObjectBody(new ByteArrayInputStream(data)) .build(); objectStorageClient.putObject(request); return buildObjectUri(container, objectKey); @@ -305,7 +311,7 @@ private String generatePreauthenticatedRequest(String container, String objectKe int ttlSeconds, CreatePreauthenticatedRequestDetails.AccessType accessType) { try { - Date expiry = new Date(System.currentTimeMillis() + (long) ttlSeconds * 1000); + Date expiry = Date.from(java.time.Instant.now().plusSeconds(ttlSeconds)); CreatePreauthenticatedRequestDetails details = CreatePreauthenticatedRequestDetails.builder() .name("par-" + objectKey + "-" + System.currentTimeMillis()) @@ -358,22 +364,6 @@ private String buildObjectUri(String container, String objectKey) { namespace + "/b/" + container + "/o/" + objectKey; } - private String computeMd5Base64(File file) { - try { - MessageDigest md = MessageDigest.getInstance("MD5"); - try (FileInputStream fis = new FileInputStream(file)) { - byte[] buffer = new byte[8192]; - int read; - while ((read = fis.read(buffer)) != -1) { - md.update(buffer, 0, read); - } - } - return Base64.getEncoder().encodeToString(md.digest()); - } catch (Exception e) { - throw new StorageServiceException("Failed to compute MD5: " + e.getMessage(), e); - } - } - private String computeMd5Base64(byte[] content) { try { MessageDigest md = MessageDigest.getInstance("MD5"); From 4c3502f214e9bcdf1b67f32d8fde39f8c0e09630 Mon Sep 17 00:00:00 2001 From: Aiman Sharief Date: Tue, 2 Jun 2026 19:03:38 +0530 Subject: [PATCH 44/47] fix: Use OkHttp transport for Azure to bypass reactor-netty %2F rejection Azure SDK percent-encodes '/' in blob names as '%2F' in the request line. Reactor-netty's HttpUtil.validateRequestLineTokens rejects '%2F' in DefaultFullHttpRequest, breaking single-shot REST ops (e.g. copyFromUrl) on nested blob paths. Switching the Azure BlobServiceClient to the OkHttp HTTP transport avoids that validation while preserving all other SDK behaviour. Bump version to 2.0.2-beta. Co-Authored-By: Claude Opus 4.7 --- cloud-storage-sdk-api/pom.xml | 2 +- cloud-storage-sdk-aws/pom.xml | 2 +- cloud-storage-sdk-azure/pom.xml | 9 ++++++--- .../cloud/storage/service/azure/AzureStorageService.java | 9 ++++++++- cloud-storage-sdk-gcp/pom.xml | 2 +- cloud-storage-sdk-oci/pom.xml | 2 +- pom.xml | 8 +++++++- 7 files changed, 25 insertions(+), 9 deletions(-) diff --git a/cloud-storage-sdk-api/pom.xml b/cloud-storage-sdk-api/pom.xml index 3dfac1d..3b5c29f 100644 --- a/cloud-storage-sdk-api/pom.xml +++ b/cloud-storage-sdk-api/pom.xml @@ -5,7 +5,7 @@ org.sunbird cloud-storage-sdk-parent - 2.0.1 + 2.0.2-beta cloud-storage-sdk-api diff --git a/cloud-storage-sdk-aws/pom.xml b/cloud-storage-sdk-aws/pom.xml index ef0e265..71a1a2a 100644 --- a/cloud-storage-sdk-aws/pom.xml +++ b/cloud-storage-sdk-aws/pom.xml @@ -5,7 +5,7 @@ org.sunbird cloud-storage-sdk-parent - 2.0.1 + 2.0.2-beta cloud-storage-sdk-aws diff --git a/cloud-storage-sdk-azure/pom.xml b/cloud-storage-sdk-azure/pom.xml index 1609cfb..42bff1e 100644 --- a/cloud-storage-sdk-azure/pom.xml +++ b/cloud-storage-sdk-azure/pom.xml @@ -5,7 +5,7 @@ org.sunbird cloud-storage-sdk-parent - 2.0.1 + 2.0.2-beta cloud-storage-sdk-azure @@ -26,6 +26,10 @@ com.azure azure-identity + + com.azure + azure-core-http-okhttp + org.sunbird cloud-storage-sdk-api @@ -41,6 +45,5 @@ org.mockito mockito-core test - - + diff --git a/cloud-storage-sdk-azure/src/main/java/org/sunbird/cloud/storage/service/azure/AzureStorageService.java b/cloud-storage-sdk-azure/src/main/java/org/sunbird/cloud/storage/service/azure/AzureStorageService.java index efe2a1d..4d7818f 100644 --- a/cloud-storage-sdk-azure/src/main/java/org/sunbird/cloud/storage/service/azure/AzureStorageService.java +++ b/cloud-storage-sdk-azure/src/main/java/org/sunbird/cloud/storage/service/azure/AzureStorageService.java @@ -50,7 +50,14 @@ public AzureStorageService(StorageConfig config) { String accountName = config.getStorageKey(); String endpoint = "https://" + accountName + ".blob.core.windows.net"; - BlobServiceClientBuilder builder = new BlobServiceClientBuilder().endpoint(endpoint); + // Use OkHttp transport instead of default reactor-netty. + // Reason: reactor-netty's HttpUtil.validateRequestLineTokens rejects '%2F' in + // DefaultFullHttpRequest. Azure SDK percent-encodes '/' in blob names as '%2F' + // in the request line, which breaks single-shot REST ops (e.g. copyFromUrl) on + // nested blob paths. OkHttp transport does not enforce that validation. + BlobServiceClientBuilder builder = new BlobServiceClientBuilder() + .endpoint(endpoint) + .httpClient(new com.azure.core.http.okhttp.OkHttpAsyncHttpClientBuilder().build()); switch (config.getAuthType()) { case ACCESS_KEY: diff --git a/cloud-storage-sdk-gcp/pom.xml b/cloud-storage-sdk-gcp/pom.xml index d9c49a2..a9c3191 100644 --- a/cloud-storage-sdk-gcp/pom.xml +++ b/cloud-storage-sdk-gcp/pom.xml @@ -5,7 +5,7 @@ org.sunbird cloud-storage-sdk-parent - 2.0.1 + 2.0.2-beta cloud-storage-sdk-gcp diff --git a/cloud-storage-sdk-oci/pom.xml b/cloud-storage-sdk-oci/pom.xml index 9094b15..1caf071 100644 --- a/cloud-storage-sdk-oci/pom.xml +++ b/cloud-storage-sdk-oci/pom.xml @@ -5,7 +5,7 @@ org.sunbird cloud-storage-sdk-parent - 2.0.1 + 2.0.2-beta cloud-storage-sdk-oci diff --git a/pom.xml b/pom.xml index ccec63a..4f4af06 100644 --- a/pom.xml +++ b/pom.xml @@ -4,7 +4,7 @@ 4.0.0 org.sunbird cloud-storage-sdk-parent - 2.0.1 + 2.0.2-beta pom Cloud Storage SDK Parent Multi-module cloud storage SDK providing CSP-specific implementations using official cloud SDKs. @@ -20,6 +20,7 @@ 2.25.0 12.25.0 1.11.0 + 1.11.20 2.30.0 3.30.0 2.9.0 @@ -126,6 +127,11 @@ azure-identity ${azure.identity.version} + + com.azure + azure-core-http-okhttp + ${azure.core.http.okhttp.version} + From f7c0884a81d3929f4c1174ae8d9c3fa72b977df4 Mon Sep 17 00:00:00 2001 From: Aiman Sharief Date: Wed, 3 Jun 2026 12:20:11 +0530 Subject: [PATCH 45/47] fix: Exclude azure-core-http-netty and align okhttp to azure-core 1.45.0 Exclude the transitive azure-core-http-netty from azure-storage-blob and azure-identity so the SDK ships only the OkHttp HTTP transport. This removes reactor-netty entirely from the SDK classpath, eliminating any future netty validation regressions (e.g. the 4.1.129 bitmask bug where 1L << c wraps modulo 64 and rejects uppercase ASCII chars J/M/backtick as LF/CR/SPACE). Downgrade azure-core-http-okhttp 1.11.20 -> 1.11.0 to match the azure-core 1.45.0 bundled by downstream consumers (asset-enrichment fat jar in knowledge-platform-jobs). 1.11.20 calls BinaryData.writeTo(WritableByteChannel) which was added in azure-core 1.49.0 and triggers NoSuchMethodError at runtime. Co-Authored-By: Claude Opus 4.7 --- cloud-storage-sdk-azure/pom.xml | 15 ++++++++++++++- pom.xml | 2 +- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/cloud-storage-sdk-azure/pom.xml b/cloud-storage-sdk-azure/pom.xml index 42bff1e..dc14983 100644 --- a/cloud-storage-sdk-azure/pom.xml +++ b/cloud-storage-sdk-azure/pom.xml @@ -21,10 +21,22 @@ com.azure azure-storage-blob + + + com.azure + azure-core-http-netty + + com.azure azure-identity + + + com.azure + azure-core-http-netty + + com.azure @@ -45,5 +57,6 @@ org.mockito mockito-core test - + + diff --git a/pom.xml b/pom.xml index 4f4af06..eaffe7f 100644 --- a/pom.xml +++ b/pom.xml @@ -20,7 +20,7 @@ 2.25.0 12.25.0 1.11.0 - 1.11.20 + 1.11.0 2.30.0 3.30.0 2.9.0 From 34d051e28e36e461dbe3e57533b7c7090dc29503 Mon Sep 17 00:00:00 2001 From: Aiman Sharief Date: Wed, 3 Jun 2026 13:20:44 +0530 Subject: [PATCH 46/47] fix: Bump to newer version --- cloud-storage-sdk-api/pom.xml | 2 +- cloud-storage-sdk-aws/pom.xml | 2 +- cloud-storage-sdk-azure/pom.xml | 2 +- cloud-storage-sdk-gcp/pom.xml | 2 +- cloud-storage-sdk-oci/pom.xml | 2 +- pom.xml | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/cloud-storage-sdk-api/pom.xml b/cloud-storage-sdk-api/pom.xml index 3b5c29f..4d32bb0 100644 --- a/cloud-storage-sdk-api/pom.xml +++ b/cloud-storage-sdk-api/pom.xml @@ -5,7 +5,7 @@ org.sunbird cloud-storage-sdk-parent - 2.0.2-beta + 2.0.2 cloud-storage-sdk-api diff --git a/cloud-storage-sdk-aws/pom.xml b/cloud-storage-sdk-aws/pom.xml index 71a1a2a..7ce8d77 100644 --- a/cloud-storage-sdk-aws/pom.xml +++ b/cloud-storage-sdk-aws/pom.xml @@ -5,7 +5,7 @@ org.sunbird cloud-storage-sdk-parent - 2.0.2-beta + 2.0.2 cloud-storage-sdk-aws diff --git a/cloud-storage-sdk-azure/pom.xml b/cloud-storage-sdk-azure/pom.xml index dc14983..8e3c3c5 100644 --- a/cloud-storage-sdk-azure/pom.xml +++ b/cloud-storage-sdk-azure/pom.xml @@ -5,7 +5,7 @@ org.sunbird cloud-storage-sdk-parent - 2.0.2-beta + 2.0.2 cloud-storage-sdk-azure diff --git a/cloud-storage-sdk-gcp/pom.xml b/cloud-storage-sdk-gcp/pom.xml index a9c3191..6be746d 100644 --- a/cloud-storage-sdk-gcp/pom.xml +++ b/cloud-storage-sdk-gcp/pom.xml @@ -5,7 +5,7 @@ org.sunbird cloud-storage-sdk-parent - 2.0.2-beta + 2.0.2 cloud-storage-sdk-gcp diff --git a/cloud-storage-sdk-oci/pom.xml b/cloud-storage-sdk-oci/pom.xml index 1caf071..a60d651 100644 --- a/cloud-storage-sdk-oci/pom.xml +++ b/cloud-storage-sdk-oci/pom.xml @@ -5,7 +5,7 @@ org.sunbird cloud-storage-sdk-parent - 2.0.2-beta + 2.0.2 cloud-storage-sdk-oci diff --git a/pom.xml b/pom.xml index eaffe7f..13851f5 100644 --- a/pom.xml +++ b/pom.xml @@ -4,7 +4,7 @@ 4.0.0 org.sunbird cloud-storage-sdk-parent - 2.0.2-beta + 2.0.2 pom Cloud Storage SDK Parent Multi-module cloud storage SDK providing CSP-specific implementations using official cloud SDKs. From a60d129f6a13716bfd6f21e7e990fa82a3d58a7b Mon Sep 17 00:00:00 2001 From: Aiman Sharief Date: Wed, 3 Jun 2026 16:39:14 +0530 Subject: [PATCH 47/47] fix: Use OkHttp transport for Azure SDK Switch AzureStorageService to use the azure-core OkHttp async HTTP client (OkHttpAsyncHttpClientBuilder) to avoid reactor-netty's request-line validation bug that can reject percent-encoded/uppercase characters in blob names. Add and pin azure.core.http.okhttp.version to 1.11.0 in pom.xml with a comment noting newer okhttp bindings require azure-core >=1.49.0 while the build currently bundles azure-core 1.45.0. --- .../cloud/storage/service/azure/AzureStorageService.java | 9 +++------ pom.xml | 1 + 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/cloud-storage-sdk-azure/src/main/java/org/sunbird/cloud/storage/service/azure/AzureStorageService.java b/cloud-storage-sdk-azure/src/main/java/org/sunbird/cloud/storage/service/azure/AzureStorageService.java index 4d7818f..0eaeb88 100644 --- a/cloud-storage-sdk-azure/src/main/java/org/sunbird/cloud/storage/service/azure/AzureStorageService.java +++ b/cloud-storage-sdk-azure/src/main/java/org/sunbird/cloud/storage/service/azure/AzureStorageService.java @@ -17,6 +17,7 @@ import com.azure.storage.blob.sas.BlobSasPermission; import com.azure.storage.blob.sas.BlobServiceSasSignatureValues; import com.azure.storage.common.StorageSharedKeyCredential; +import com.azure.core.http.okhttp.OkHttpAsyncHttpClientBuilder; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.sunbird.cloud.storage.AbstractStorageService; @@ -50,14 +51,10 @@ public AzureStorageService(StorageConfig config) { String accountName = config.getStorageKey(); String endpoint = "https://" + accountName + ".blob.core.windows.net"; - // Use OkHttp transport instead of default reactor-netty. - // Reason: reactor-netty's HttpUtil.validateRequestLineTokens rejects '%2F' in - // DefaultFullHttpRequest. Azure SDK percent-encodes '/' in blob names as '%2F' - // in the request line, which breaks single-shot REST ops (e.g. copyFromUrl) on - // nested blob paths. OkHttp transport does not enforce that validation. + // OkHttp avoids Netty's request-line bitmask bug that rejects uppercase chars in percent-encoded blob names. BlobServiceClientBuilder builder = new BlobServiceClientBuilder() .endpoint(endpoint) - .httpClient(new com.azure.core.http.okhttp.OkHttpAsyncHttpClientBuilder().build()); + .httpClient(new OkHttpAsyncHttpClientBuilder().build()); switch (config.getAuthType()) { case ACCESS_KEY: diff --git a/pom.xml b/pom.xml index 13851f5..4bf20b2 100644 --- a/pom.xml +++ b/pom.xml @@ -20,6 +20,7 @@ 2.25.0 12.25.0 1.11.0 + 1.11.0 2.30.0 3.30.0