[
  {
    "path": ".gitignore",
    "content": "HELP.md\ntarget/\n!.mvn/wrapper/maven-wrapper.jar\n!**/src/main/**\n!**/src/test/**\n\n### STS ###\n.apt_generated\n.classpath\n.factorypath\n.project\n.settings\n.springBeans\n.sts4-cache\n\n### IntelliJ IDEA ###\n.idea\n*.iws\n*.iml\n*.ipr\n\n### NetBeans ###\n/nbproject/private/\n/nbbuild/\n/dist/\n/nbdist/\n/.nb-gradle/\nbuild/\n\n### VS Code ###\n.vscode/\n"
  },
  {
    "path": ".mvn/wrapper/maven-wrapper.properties",
    "content": "# Licensed to the Apache Software Foundation (ASF) under one\n# or more contributor license agreements.  See the NOTICE file\n# distributed with this work for additional information\n# regarding copyright ownership.  The ASF licenses this file\n# to you under the Apache License, Version 2.0 (the\n# \"License\"); you may not use this file except in compliance\n# with the License.  You may obtain a copy of the License at\n#\n#   http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing,\n# software distributed under the License is distributed on an\n# \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n# KIND, either express or implied.  See the License for the\n# specific language governing permissions and limitations\n# under the License.\nwrapperVersion=3.3.2\ndistributionType=only-script\ndistributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.9/apache-maven-3.9.9-bin.zip\n"
  },
  {
    "path": "Dockerfile",
    "content": "FROM maven:3.9.9-amazoncorretto-23-alpine AS build\nLABEL authors=\"Saravanakumar Arunachalam\"\n\nCOPY . .\n\nRUN mvn clean package\n\nFROM amazoncorretto:23-jdk\nWORKDIR /app\nCOPY --from=build target/video-stream.jar .\nCOPY --from=build target/classes/video/toystory.mp4 ./video/\nEXPOSE 8080\n\nENTRYPOINT java $JAVA_OPTS -jar ./video-stream.jar"
  },
  {
    "path": "README.md",
    "content": "# Video Service Overview\n\nThis service is designed to manage video files by providing CRUD (Create, Read, Update, Delete) operations and enabling HTTP-based streaming of videos.\n\n## Features\n- **Video Management**: Perform CRUD operations on video files.\n- **HTTP Streaming**: Stream video content directly over HTTP.\n\n## Local Setup\nTo run the service locally, you can launch it either as a Java application or within a Docker container.\n\n### Configuration\n- **Port**: The application listens on port `8080` by default.\n- **Context Path and API Endpoint**: The base context path is `/video-service`, with an API prefix of `/api/v1/videos`.\n\n### Prerequisites\nEnsure the following dependencies are installed:\n- Java 17 or higher\n- Docker and Docker Compose (if running in a containerized environment)\n\n### Running the Application\n#### Using Java\n1. Clone the repository:\n   ```bash\n   git clone <repository_url>\n   cd <repository_directory>\n   ```\n2. Build and run the application using your preferred IDE or a build tool like Maven:\n   ```bash\n   mvn spring-boot:run\n   ```\n\n#### Using Docker\n1. Verify that Docker and Docker Compose are installed on your system. If not, refer to the official installation guides for [Docker](https://docs.docker.com/get-docker/) and [Docker Compose](https://docs.docker.com/compose/install/).\n2. Build and run the Docker container:\n   ```bash\n   docker-compose build\n   docker-compose up\n   ```\n\n#### Adding Video Files\n- **Default Directory**: The application uses the `./video` directory for video content by default.\n- **Custom Directory**: To use a custom directory, set the `VIDEO_CONTENT_PATH` environment variable to point to your desired location.\n- **Mounting Volumes**: When running in Docker, mount the desired video folder as a volume in the container to include additional video files.\n\nExample of mounting a directory: docker-compose.yaml\n```yaml\nservices:\n  video-service:\n    volumes:\n      - /path/to/local/video/folder:/tmp/content\n```\n\n## Streaming Endpoint\nThe application provides an HTTP streaming endpoint to serve video content.\n\n### Endpoint Format\n- URL: `/video-service/api/v1/videos` - Will list the all video files in the directory\n- URL: `/video-service/api/v1/videos/stream/{fileType}/{fileName}`\n    - **`fileType`**: The format of the video file (e.g., `mp4`, `mkv`).\n    - **`fileName`**: The name of the video file without the extension.\n- URL: `/video-service/api/v1/videos/object-key/{objectKey}` - prefer to use this endpoint\n    - **`objectKey`**: Object key of the file, can get the object key by hitting the `/video-service/api/v1/videos` \n\n### Example Request\nOnce the application is running, you can access the streaming service with the following format:\n```\nhttp://{host}:{port}/video-service/api/v1/videos/stream/{fileType}/{fileName}\n```\nFor example:\n```\nhttp://localhost:8080/video-service/api/v1/videos/stream/mp4/toystory\n```\n\n### Customizing the Video Directory\nTo change the default video content directory:\n1. Set the `VIDEO_CONTENT_PATH` environment variable.\n   ```bash\n   export VIDEO_CONTENT_PATH=/path/to/your/video/directory\n   ```\n2. Restart the application to apply the changes.\n\n## Testing and Debugging\n### Unit and Integration Tests\n- The project includes unit and integration tests to ensure functionality. Use the following command to execute tests:\n   ```bash\n   mvn test\n   ```\n\n### Logging\n- The application uses configurable logging via `logback.xml`. Logs are stored in the `logs` directory by default.\n- To change the logging level, modify the `application.properties` file or the `logback.xml` configuration.\n\n### Common Issues\n1. **Port Already in Use**:\n    - If port `8080` is occupied, modify the port in the `application.properties` file:\n      ```properties\n      server.port=9090\n      ```\n2. **File Not Found**:\n    - Ensure that video files are present in the specified directory.\n    - Verify the `VIDEO_CONTENT_PATH` environment variable if using a custom directory.\n\n\n## Further Reading\nFor an in-depth explanation of how this service is implemented, including details on HTTP-based video streaming using Spring Boot, check out [this Medium article](https://medium.com/@saravanastar/video-streaming-over-http-using-spring-boot-51e9830a3b8).\n\n"
  },
  {
    "path": "docker-compose.yaml",
    "content": "services:\n  video-service:\n    build:\n      context: ./\n      dockerfile: Dockerfile\n    volumes:\n      - ./video:/tmp/content\n    environment:\n      VIDEO_CONTENT_PATH: /tmp/content\n    ports:\n      - 8080:8080\n\n\n"
  },
  {
    "path": "mvnw",
    "content": "#!/bin/sh\n# ----------------------------------------------------------------------------\n# Licensed to the Apache Software Foundation (ASF) under one\n# or more contributor license agreements.  See the NOTICE file\n# distributed with this work for additional information\n# regarding copyright ownership.  The ASF licenses this file\n# to you under the Apache License, Version 2.0 (the\n# \"License\"); you may not use this file except in compliance\n# with the License.  You may obtain a copy of the License at\n#\n#    http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing,\n# software distributed under the License is distributed on an\n# \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n# KIND, either express or implied.  See the License for the\n# specific language governing permissions and limitations\n# under the License.\n# ----------------------------------------------------------------------------\n\n# ----------------------------------------------------------------------------\n# Apache Maven Wrapper startup batch script, version 3.3.2\n#\n# Optional ENV vars\n# -----------------\n#   JAVA_HOME - location of a JDK home dir, required when download maven via java source\n#   MVNW_REPOURL - repo url base for downloading maven distribution\n#   MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven\n#   MVNW_VERBOSE - true: enable verbose log; debug: trace the mvnw script; others: silence the output\n# ----------------------------------------------------------------------------\n\nset -euf\n[ \"${MVNW_VERBOSE-}\" != debug ] || set -x\n\n# OS specific support.\nnative_path() { printf %s\\\\n \"$1\"; }\ncase \"$(uname)\" in\nCYGWIN* | MINGW*)\n  [ -z \"${JAVA_HOME-}\" ] || JAVA_HOME=\"$(cygpath --unix \"$JAVA_HOME\")\"\n  native_path() { cygpath --path --windows \"$1\"; }\n  ;;\nesac\n\n# set JAVACMD and JAVACCMD\nset_java_home() {\n  # For Cygwin and MinGW, ensure paths are in Unix format before anything is touched\n  if [ -n \"${JAVA_HOME-}\" ]; then\n    if [ -x \"$JAVA_HOME/jre/sh/java\" ]; then\n      # IBM's JDK on AIX uses strange locations for the executables\n      JAVACMD=\"$JAVA_HOME/jre/sh/java\"\n      JAVACCMD=\"$JAVA_HOME/jre/sh/javac\"\n    else\n      JAVACMD=\"$JAVA_HOME/bin/java\"\n      JAVACCMD=\"$JAVA_HOME/bin/javac\"\n\n      if [ ! -x \"$JAVACMD\" ] || [ ! -x \"$JAVACCMD\" ]; then\n        echo \"The JAVA_HOME environment variable is not defined correctly, so mvnw cannot run.\" >&2\n        echo \"JAVA_HOME is set to \\\"$JAVA_HOME\\\", but \\\"\\$JAVA_HOME/bin/java\\\" or \\\"\\$JAVA_HOME/bin/javac\\\" does not exist.\" >&2\n        return 1\n      fi\n    fi\n  else\n    JAVACMD=\"$(\n      'set' +e\n      'unset' -f command 2>/dev/null\n      'command' -v java\n    )\" || :\n    JAVACCMD=\"$(\n      'set' +e\n      'unset' -f command 2>/dev/null\n      'command' -v javac\n    )\" || :\n\n    if [ ! -x \"${JAVACMD-}\" ] || [ ! -x \"${JAVACCMD-}\" ]; then\n      echo \"The java/javac command does not exist in PATH nor is JAVA_HOME set, so mvnw cannot run.\" >&2\n      return 1\n    fi\n  fi\n}\n\n# hash string like Java String::hashCode\nhash_string() {\n  str=\"${1:-}\" h=0\n  while [ -n \"$str\" ]; do\n    char=\"${str%\"${str#?}\"}\"\n    h=$(((h * 31 + $(LC_CTYPE=C printf %d \"'$char\")) % 4294967296))\n    str=\"${str#?}\"\n  done\n  printf %x\\\\n $h\n}\n\nverbose() { :; }\n[ \"${MVNW_VERBOSE-}\" != true ] || verbose() { printf %s\\\\n \"${1-}\"; }\n\ndie() {\n  printf %s\\\\n \"$1\" >&2\n  exit 1\n}\n\ntrim() {\n  # MWRAPPER-139:\n  #   Trims trailing and leading whitespace, carriage returns, tabs, and linefeeds.\n  #   Needed for removing poorly interpreted newline sequences when running in more\n  #   exotic environments such as mingw bash on Windows.\n  printf \"%s\" \"${1}\" | tr -d '[:space:]'\n}\n\n# parse distributionUrl and optional distributionSha256Sum, requires .mvn/wrapper/maven-wrapper.properties\nwhile IFS=\"=\" read -r key value; do\n  case \"${key-}\" in\n  distributionUrl) distributionUrl=$(trim \"${value-}\") ;;\n  distributionSha256Sum) distributionSha256Sum=$(trim \"${value-}\") ;;\n  esac\ndone <\"${0%/*}/.mvn/wrapper/maven-wrapper.properties\"\n[ -n \"${distributionUrl-}\" ] || die \"cannot read distributionUrl property in ${0%/*}/.mvn/wrapper/maven-wrapper.properties\"\n\ncase \"${distributionUrl##*/}\" in\nmaven-mvnd-*bin.*)\n  MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/\n  case \"${PROCESSOR_ARCHITECTURE-}${PROCESSOR_ARCHITEW6432-}:$(uname -a)\" in\n  *AMD64:CYGWIN* | *AMD64:MINGW*) distributionPlatform=windows-amd64 ;;\n  :Darwin*x86_64) distributionPlatform=darwin-amd64 ;;\n  :Darwin*arm64) distributionPlatform=darwin-aarch64 ;;\n  :Linux*x86_64*) distributionPlatform=linux-amd64 ;;\n  *)\n    echo \"Cannot detect native platform for mvnd on $(uname)-$(uname -m), use pure java version\" >&2\n    distributionPlatform=linux-amd64\n    ;;\n  esac\n  distributionUrl=\"${distributionUrl%-bin.*}-$distributionPlatform.zip\"\n  ;;\nmaven-mvnd-*) MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ ;;\n*) MVN_CMD=\"mvn${0##*/mvnw}\" _MVNW_REPO_PATTERN=/org/apache/maven/ ;;\nesac\n\n# apply MVNW_REPOURL and calculate MAVEN_HOME\n# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-<version>,maven-mvnd-<version>-<platform>}/<hash>\n[ -z \"${MVNW_REPOURL-}\" ] || distributionUrl=\"$MVNW_REPOURL$_MVNW_REPO_PATTERN${distributionUrl#*\"$_MVNW_REPO_PATTERN\"}\"\ndistributionUrlName=\"${distributionUrl##*/}\"\ndistributionUrlNameMain=\"${distributionUrlName%.*}\"\ndistributionUrlNameMain=\"${distributionUrlNameMain%-bin}\"\nMAVEN_USER_HOME=\"${MAVEN_USER_HOME:-${HOME}/.m2}\"\nMAVEN_HOME=\"${MAVEN_USER_HOME}/wrapper/dists/${distributionUrlNameMain-}/$(hash_string \"$distributionUrl\")\"\n\nexec_maven() {\n  unset MVNW_VERBOSE MVNW_USERNAME MVNW_PASSWORD MVNW_REPOURL || :\n  exec \"$MAVEN_HOME/bin/$MVN_CMD\" \"$@\" || die \"cannot exec $MAVEN_HOME/bin/$MVN_CMD\"\n}\n\nif [ -d \"$MAVEN_HOME\" ]; then\n  verbose \"found existing MAVEN_HOME at $MAVEN_HOME\"\n  exec_maven \"$@\"\nfi\n\ncase \"${distributionUrl-}\" in\n*?-bin.zip | *?maven-mvnd-?*-?*.zip) ;;\n*) die \"distributionUrl is not valid, must match *-bin.zip or maven-mvnd-*.zip, but found '${distributionUrl-}'\" ;;\nesac\n\n# prepare tmp dir\nif TMP_DOWNLOAD_DIR=\"$(mktemp -d)\" && [ -d \"$TMP_DOWNLOAD_DIR\" ]; then\n  clean() { rm -rf -- \"$TMP_DOWNLOAD_DIR\"; }\n  trap clean HUP INT TERM EXIT\nelse\n  die \"cannot create temp dir\"\nfi\n\nmkdir -p -- \"${MAVEN_HOME%/*}\"\n\n# Download and Install Apache Maven\nverbose \"Couldn't find MAVEN_HOME, downloading and installing it ...\"\nverbose \"Downloading from: $distributionUrl\"\nverbose \"Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName\"\n\n# select .zip or .tar.gz\nif ! command -v unzip >/dev/null; then\n  distributionUrl=\"${distributionUrl%.zip}.tar.gz\"\n  distributionUrlName=\"${distributionUrl##*/}\"\nfi\n\n# verbose opt\n__MVNW_QUIET_WGET=--quiet __MVNW_QUIET_CURL=--silent __MVNW_QUIET_UNZIP=-q __MVNW_QUIET_TAR=''\n[ \"${MVNW_VERBOSE-}\" != true ] || __MVNW_QUIET_WGET='' __MVNW_QUIET_CURL='' __MVNW_QUIET_UNZIP='' __MVNW_QUIET_TAR=v\n\n# normalize http auth\ncase \"${MVNW_PASSWORD:+has-password}\" in\n'') MVNW_USERNAME='' MVNW_PASSWORD='' ;;\nhas-password) [ -n \"${MVNW_USERNAME-}\" ] || MVNW_USERNAME='' MVNW_PASSWORD='' ;;\nesac\n\nif [ -z \"${MVNW_USERNAME-}\" ] && command -v wget >/dev/null; then\n  verbose \"Found wget ... using wget\"\n  wget ${__MVNW_QUIET_WGET:+\"$__MVNW_QUIET_WGET\"} \"$distributionUrl\" -O \"$TMP_DOWNLOAD_DIR/$distributionUrlName\" || die \"wget: Failed to fetch $distributionUrl\"\nelif [ -z \"${MVNW_USERNAME-}\" ] && command -v curl >/dev/null; then\n  verbose \"Found curl ... using curl\"\n  curl ${__MVNW_QUIET_CURL:+\"$__MVNW_QUIET_CURL\"} -f -L -o \"$TMP_DOWNLOAD_DIR/$distributionUrlName\" \"$distributionUrl\" || die \"curl: Failed to fetch $distributionUrl\"\nelif set_java_home; then\n  verbose \"Falling back to use Java to download\"\n  javaSource=\"$TMP_DOWNLOAD_DIR/Downloader.java\"\n  targetZip=\"$TMP_DOWNLOAD_DIR/$distributionUrlName\"\n  cat >\"$javaSource\" <<-END\n\tpublic class Downloader extends java.net.Authenticator\n\t{\n\t  protected java.net.PasswordAuthentication getPasswordAuthentication()\n\t  {\n\t    return new java.net.PasswordAuthentication( System.getenv( \"MVNW_USERNAME\" ), System.getenv( \"MVNW_PASSWORD\" ).toCharArray() );\n\t  }\n\t  public static void main( String[] args ) throws Exception\n\t  {\n\t    setDefault( new Downloader() );\n\t    java.nio.file.Files.copy( java.net.URI.create( args[0] ).toURL().openStream(), java.nio.file.Paths.get( args[1] ).toAbsolutePath().normalize() );\n\t  }\n\t}\n\tEND\n  # For Cygwin/MinGW, switch paths to Windows format before running javac and java\n  verbose \" - Compiling Downloader.java ...\"\n  \"$(native_path \"$JAVACCMD\")\" \"$(native_path \"$javaSource\")\" || die \"Failed to compile Downloader.java\"\n  verbose \" - Running Downloader.java ...\"\n  \"$(native_path \"$JAVACMD\")\" -cp \"$(native_path \"$TMP_DOWNLOAD_DIR\")\" Downloader \"$distributionUrl\" \"$(native_path \"$targetZip\")\"\nfi\n\n# If specified, validate the SHA-256 sum of the Maven distribution zip file\nif [ -n \"${distributionSha256Sum-}\" ]; then\n  distributionSha256Result=false\n  if [ \"$MVN_CMD\" = mvnd.sh ]; then\n    echo \"Checksum validation is not supported for maven-mvnd.\" >&2\n    echo \"Please disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties.\" >&2\n    exit 1\n  elif command -v sha256sum >/dev/null; then\n    if echo \"$distributionSha256Sum  $TMP_DOWNLOAD_DIR/$distributionUrlName\" | sha256sum -c >/dev/null 2>&1; then\n      distributionSha256Result=true\n    fi\n  elif command -v shasum >/dev/null; then\n    if echo \"$distributionSha256Sum  $TMP_DOWNLOAD_DIR/$distributionUrlName\" | shasum -a 256 -c >/dev/null 2>&1; then\n      distributionSha256Result=true\n    fi\n  else\n    echo \"Checksum validation was requested but neither 'sha256sum' or 'shasum' are available.\" >&2\n    echo \"Please install either command, or disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties.\" >&2\n    exit 1\n  fi\n  if [ $distributionSha256Result = false ]; then\n    echo \"Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised.\" >&2\n    echo \"If you updated your Maven version, you need to update the specified distributionSha256Sum property.\" >&2\n    exit 1\n  fi\nfi\n\n# unzip and move\nif command -v unzip >/dev/null; then\n  unzip ${__MVNW_QUIET_UNZIP:+\"$__MVNW_QUIET_UNZIP\"} \"$TMP_DOWNLOAD_DIR/$distributionUrlName\" -d \"$TMP_DOWNLOAD_DIR\" || die \"failed to unzip\"\nelse\n  tar xzf${__MVNW_QUIET_TAR:+\"$__MVNW_QUIET_TAR\"} \"$TMP_DOWNLOAD_DIR/$distributionUrlName\" -C \"$TMP_DOWNLOAD_DIR\" || die \"failed to untar\"\nfi\nprintf %s\\\\n \"$distributionUrl\" >\"$TMP_DOWNLOAD_DIR/$distributionUrlNameMain/mvnw.url\"\nmv -- \"$TMP_DOWNLOAD_DIR/$distributionUrlNameMain\" \"$MAVEN_HOME\" || [ -d \"$MAVEN_HOME\" ] || die \"fail to move MAVEN_HOME\"\n\nclean || :\nexec_maven \"$@\"\n"
  },
  {
    "path": "mvnw.cmd",
    "content": "<# : batch portion\n@REM ----------------------------------------------------------------------------\n@REM Licensed to the Apache Software Foundation (ASF) under one\n@REM or more contributor license agreements.  See the NOTICE file\n@REM distributed with this work for additional information\n@REM regarding copyright ownership.  The ASF licenses this file\n@REM to you under the Apache License, Version 2.0 (the\n@REM \"License\"); you may not use this file except in compliance\n@REM with the License.  You may obtain a copy of the License at\n@REM\n@REM    http://www.apache.org/licenses/LICENSE-2.0\n@REM\n@REM Unless required by applicable law or agreed to in writing,\n@REM software distributed under the License is distributed on an\n@REM \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n@REM KIND, either express or implied.  See the License for the\n@REM specific language governing permissions and limitations\n@REM under the License.\n@REM ----------------------------------------------------------------------------\n\n@REM ----------------------------------------------------------------------------\n@REM Apache Maven Wrapper startup batch script, version 3.3.2\n@REM\n@REM Optional ENV vars\n@REM   MVNW_REPOURL - repo url base for downloading maven distribution\n@REM   MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven\n@REM   MVNW_VERBOSE - true: enable verbose log; others: silence the output\n@REM ----------------------------------------------------------------------------\n\n@IF \"%__MVNW_ARG0_NAME__%\"==\"\" (SET __MVNW_ARG0_NAME__=%~nx0)\n@SET __MVNW_CMD__=\n@SET __MVNW_ERROR__=\n@SET __MVNW_PSMODULEP_SAVE=%PSModulePath%\n@SET PSModulePath=\n@FOR /F \"usebackq tokens=1* delims==\" %%A IN (`powershell -noprofile \"& {$scriptDir='%~dp0'; $script='%__MVNW_ARG0_NAME__%'; icm -ScriptBlock ([Scriptblock]::Create((Get-Content -Raw '%~f0'))) -NoNewScope}\"`) DO @(\n  IF \"%%A\"==\"MVN_CMD\" (set __MVNW_CMD__=%%B) ELSE IF \"%%B\"==\"\" (echo %%A) ELSE (echo %%A=%%B)\n)\n@SET PSModulePath=%__MVNW_PSMODULEP_SAVE%\n@SET __MVNW_PSMODULEP_SAVE=\n@SET __MVNW_ARG0_NAME__=\n@SET MVNW_USERNAME=\n@SET MVNW_PASSWORD=\n@IF NOT \"%__MVNW_CMD__%\"==\"\" (%__MVNW_CMD__% %*)\n@echo Cannot start maven from wrapper >&2 && exit /b 1\n@GOTO :EOF\n: end batch / begin powershell #>\n\n$ErrorActionPreference = \"Stop\"\nif ($env:MVNW_VERBOSE -eq \"true\") {\n  $VerbosePreference = \"Continue\"\n}\n\n# calculate distributionUrl, requires .mvn/wrapper/maven-wrapper.properties\n$distributionUrl = (Get-Content -Raw \"$scriptDir/.mvn/wrapper/maven-wrapper.properties\" | ConvertFrom-StringData).distributionUrl\nif (!$distributionUrl) {\n  Write-Error \"cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties\"\n}\n\nswitch -wildcard -casesensitive ( $($distributionUrl -replace '^.*/','') ) {\n  \"maven-mvnd-*\" {\n    $USE_MVND = $true\n    $distributionUrl = $distributionUrl -replace '-bin\\.[^.]*$',\"-windows-amd64.zip\"\n    $MVN_CMD = \"mvnd.cmd\"\n    break\n  }\n  default {\n    $USE_MVND = $false\n    $MVN_CMD = $script -replace '^mvnw','mvn'\n    break\n  }\n}\n\n# apply MVNW_REPOURL and calculate MAVEN_HOME\n# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-<version>,maven-mvnd-<version>-<platform>}/<hash>\nif ($env:MVNW_REPOURL) {\n  $MVNW_REPO_PATTERN = if ($USE_MVND) { \"/org/apache/maven/\" } else { \"/maven/mvnd/\" }\n  $distributionUrl = \"$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace '^.*'+$MVNW_REPO_PATTERN,'')\"\n}\n$distributionUrlName = $distributionUrl -replace '^.*/',''\n$distributionUrlNameMain = $distributionUrlName -replace '\\.[^.]*$','' -replace '-bin$',''\n$MAVEN_HOME_PARENT = \"$HOME/.m2/wrapper/dists/$distributionUrlNameMain\"\nif ($env:MAVEN_USER_HOME) {\n  $MAVEN_HOME_PARENT = \"$env:MAVEN_USER_HOME/wrapper/dists/$distributionUrlNameMain\"\n}\n$MAVEN_HOME_NAME = ([System.Security.Cryptography.MD5]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString(\"x2\")}) -join ''\n$MAVEN_HOME = \"$MAVEN_HOME_PARENT/$MAVEN_HOME_NAME\"\n\nif (Test-Path -Path \"$MAVEN_HOME\" -PathType Container) {\n  Write-Verbose \"found existing MAVEN_HOME at $MAVEN_HOME\"\n  Write-Output \"MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD\"\n  exit $?\n}\n\nif (! $distributionUrlNameMain -or ($distributionUrlName -eq $distributionUrlNameMain)) {\n  Write-Error \"distributionUrl is not valid, must end with *-bin.zip, but found $distributionUrl\"\n}\n\n# prepare tmp dir\n$TMP_DOWNLOAD_DIR_HOLDER = New-TemporaryFile\n$TMP_DOWNLOAD_DIR = New-Item -Itemtype Directory -Path \"$TMP_DOWNLOAD_DIR_HOLDER.dir\"\n$TMP_DOWNLOAD_DIR_HOLDER.Delete() | Out-Null\ntrap {\n  if ($TMP_DOWNLOAD_DIR.Exists) {\n    try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null }\n    catch { Write-Warning \"Cannot remove $TMP_DOWNLOAD_DIR\" }\n  }\n}\n\nNew-Item -Itemtype Directory -Path \"$MAVEN_HOME_PARENT\" -Force | Out-Null\n\n# Download and Install Apache Maven\nWrite-Verbose \"Couldn't find MAVEN_HOME, downloading and installing it ...\"\nWrite-Verbose \"Downloading from: $distributionUrl\"\nWrite-Verbose \"Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName\"\n\n$webclient = New-Object System.Net.WebClient\nif ($env:MVNW_USERNAME -and $env:MVNW_PASSWORD) {\n  $webclient.Credentials = New-Object System.Net.NetworkCredential($env:MVNW_USERNAME, $env:MVNW_PASSWORD)\n}\n[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12\n$webclient.DownloadFile($distributionUrl, \"$TMP_DOWNLOAD_DIR/$distributionUrlName\") | Out-Null\n\n# If specified, validate the SHA-256 sum of the Maven distribution zip file\n$distributionSha256Sum = (Get-Content -Raw \"$scriptDir/.mvn/wrapper/maven-wrapper.properties\" | ConvertFrom-StringData).distributionSha256Sum\nif ($distributionSha256Sum) {\n  if ($USE_MVND) {\n    Write-Error \"Checksum validation is not supported for maven-mvnd. `nPlease disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties.\"\n  }\n  Import-Module $PSHOME\\Modules\\Microsoft.PowerShell.Utility -Function Get-FileHash\n  if ((Get-FileHash \"$TMP_DOWNLOAD_DIR/$distributionUrlName\" -Algorithm SHA256).Hash.ToLower() -ne $distributionSha256Sum) {\n    Write-Error \"Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised. If you updated your Maven version, you need to update the specified distributionSha256Sum property.\"\n  }\n}\n\n# unzip and move\nExpand-Archive \"$TMP_DOWNLOAD_DIR/$distributionUrlName\" -DestinationPath \"$TMP_DOWNLOAD_DIR\" | Out-Null\nRename-Item -Path \"$TMP_DOWNLOAD_DIR/$distributionUrlNameMain\" -NewName $MAVEN_HOME_NAME | Out-Null\ntry {\n  Move-Item -Path \"$TMP_DOWNLOAD_DIR/$MAVEN_HOME_NAME\" -Destination $MAVEN_HOME_PARENT | Out-Null\n} catch {\n  if (! (Test-Path -Path \"$MAVEN_HOME\" -PathType Container)) {\n    Write-Error \"fail to move MAVEN_HOME\"\n  }\n} finally {\n  try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null }\n  catch { Write-Warning \"Cannot remove $TMP_DOWNLOAD_DIR\" }\n}\n\nWrite-Output \"MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD\"\n"
  },
  {
    "path": "pom.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n         xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd\">\n    <modelVersion>4.0.0</modelVersion>\n    <parent>\n        <groupId>org.springframework.boot</groupId>\n        <artifactId>spring-boot-starter-parent</artifactId>\n        <version>3.4.0</version>\n        <relativePath/> <!-- lookup parent from repository -->\n    </parent>\n    <groupId>com.ask.home</groupId>\n    <artifactId>video-stream</artifactId>\n    <version>1.0.0-SNAPSHOT</version>\n    <name>VideoStreaming</name>\n    <description>Trying for Video Stream</description>\n\n    <properties>\n        <java.version>23</java.version>\n        <lombok.version>1.18.34</lombok.version>\n    </properties>\n\n    <dependencies>\n        <dependency>\n            <groupId>org.springframework.boot</groupId>\n            <artifactId>spring-boot-starter-actuator</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>org.springframework.boot</groupId>\n            <artifactId>spring-boot-starter-webflux</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>org.projectlombok</groupId>\n            <artifactId>lombok</artifactId>\n            <version>${lombok.version}</version>\n        </dependency>\n        <dependency>\n            <groupId>org.springframework.boot</groupId>\n            <artifactId>spring-boot-starter-test</artifactId>\n            <scope>test</scope>\n            <exclusions>\n                <exclusion>\n                    <groupId>org.junit.vintage</groupId>\n                    <artifactId>junit-vintage-engine</artifactId>\n                </exclusion>\n            </exclusions>\n        </dependency>\n        <dependency>\n            <groupId>io.projectreactor</groupId>\n            <artifactId>reactor-test</artifactId>\n            <scope>test</scope>\n        </dependency>\n    </dependencies>\n\n    <build>\n        <plugins>\n            <plugin>\n                <groupId>org.apache.maven.plugins</groupId>\n                <artifactId>maven-compiler-plugin</artifactId>\n                <version>3.13.0</version>\n                <configuration>\n                    <source>${java.version}</source>\n                    <target>${java.version}</target>\n                    <annotationProcessorPaths>\n                        <path>\n                            <groupId>org.projectlombok</groupId>\n                            <artifactId>lombok</artifactId>\n                            <version>${lombok.version}</version>\n                        </path>\n                    </annotationProcessorPaths>\n                </configuration>\n            </plugin>\n            <plugin>\n                <groupId>org.springframework.boot</groupId>\n                <artifactId>spring-boot-maven-plugin</artifactId>\n                <configuration>\n                    <finalName>video-stream</finalName>\n                </configuration>\n            </plugin>\n        </plugins>\n    </build>\n\n</project>\n"
  },
  {
    "path": "src/main/java/com/ask/home/videostream/Application.java",
    "content": "package com.ask.home.videostream;\n\nimport org.springframework.boot.SpringApplication;\nimport org.springframework.boot.autoconfigure.SpringBootApplication;\n\n@SpringBootApplication\npublic class Application {\n\n\tpublic static void main(String[] args) {\n\t\tSpringApplication.run(Application.class, args);\n\t}\n\n}\n"
  },
  {
    "path": "src/main/java/com/ask/home/videostream/adapter/ContentAdapter.java",
    "content": "package com.ask.home.videostream.adapter;\n\nimport com.ask.home.videostream.model.Content;\nimport com.ask.home.videostream.model.ContentRequest;\n\nimport java.util.List;\n\n/**\n * ContentAdapter\n */\npublic interface ContentAdapter {\n    Content getContent(ContentRequest contentRequest);\n    Long getContentSize(ContentRequest contentRequest);\n    List<Content> findAllContents();\n    Content findFileByKey(final String fileKey);\n}\n"
  },
  {
    "path": "src/main/java/com/ask/home/videostream/adapter/LocalFileContentAdapter.java",
    "content": "package com.ask.home.videostream.adapter;\n\nimport com.ask.home.videostream.model.Content;\nimport com.ask.home.videostream.model.ContentRequest;\nimport com.ask.home.videostream.util.FileUtil;\nimport lombok.extern.slf4j.Slf4j;\n\nimport java.io.File;\nimport java.io.IOException;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.nio.file.Paths;\nimport java.nio.file.attribute.BasicFileAttributes;\nimport java.util.*;\nimport java.util.stream.Collectors;\nimport java.util.stream.Stream;\n\nimport static com.ask.home.videostream.util.FileUtil.getFilePath;\n\n/**\n * Extract the content from local to the device(volume/drive).\n */\n@Slf4j\npublic class LocalFileContentAdapter implements ContentAdapter {\n\n    final public static String FILE_PATH_FORMAT = \"%s/%s\";\n    final private Map<String, Content> localFileMap;\n    final private String localFilePath;\n\n    /**\n     * Constructor injection for the root content path.\n     *\n     * @param localFilePath String.\n     */\n    public LocalFileContentAdapter(String localFilePath) {\n        this.localFilePath = localFilePath;\n        localFileMap = new HashMap<>();\n    }\n\n    /**\n     * find Object By key.\n     *\n     * @param fileKey String\n     * @return Content.\n     */\n    public Content findFileByKey(final String fileKey) {\n        if (fileKey == null) {\n            throw new RuntimeException(\"FileKey can't be null\");\n        }\n        if (localFileMap.isEmpty()) {\n            findAllContents();\n        }\n        return localFileMap.get(fileKey);\n    }\n\n\n    @Override\n    public Content getContent(final ContentRequest contentRequest) {\n        boolean isValid = validateRequest(contentRequest);\n        if (!isValid) {\n            throw new RuntimeException(\"Not a valid content request\");\n        }\n        try {\n            byte[] content = readByBytesRange(contentRequest);\n            return Content.builder().content(content).contentLength((long) content.length).rangeStart(contentRequest.getRangeStart()).rangeEnd(contentRequest.getRangeEnd()).build();\n        } catch (IOException e) {\n            throw new RuntimeException(e);\n        }\n    }\n\n\n    /**\n     * read the bytes of the file by the range.\n     *\n     * @param contentRequest ContentRequest.\n     * @return byte[]\n     * @throws IOException ioException.\n     */\n    private byte[] readByBytesRange(final ContentRequest contentRequest) throws IOException {\n        Path path = Paths.get(getFilePath(localFilePath, contentRequest.getFilePath(), contentRequest.getFileName()));\n        byte[] data = Files.readAllBytes(path);\n        if (data.length == 0) {\n            return data;\n        }\n        long end = contentRequest.getRangeEnd();\n        long start = contentRequest.getRangeStart();\n        byte[] result = new byte[(int) (end - start) + 1];\n        System.arraycopy(data, (int) start, result, 0, (int) (end - start) + 1);\n        return result;\n    }\n\n    /**\n     * Content length.\n     *\n     * @param contentRequest ContentRequest.\n     * @return Long.\n     */\n    @Override\n    public Long getContentSize(ContentRequest contentRequest) {\n        return Optional.ofNullable(contentRequest).map(_ -> Paths.get(getFilePath(localFilePath, contentRequest.getFilePath(), contentRequest.getFileName()))).map(this::sizeFromFile).orElse(0L);\n    }\n\n    @Override\n    public List<Content> findAllContents() {\n        Path path = Paths.get(new File(localFilePath).getAbsolutePath());\n        try (Stream<Path> stream = Files.walk(path, 10)) {\n            return stream.filter(file -> !Files.isDirectory(file)).map(this::prepareContent).filter(Objects::nonNull).collect(Collectors.toList());\n        } catch (IOException e) {\n            throw new RuntimeException(e);\n        }\n    }\n\n    /**\n     * prepareContent.\n     *\n     * @param path Path\n     * @return Content\n     */\n    private Content prepareContent(final Path path) {\n        if (FileUtil.isVideoFile(path)) {\n            final String fileName = path.getFileName().toString();\n            String extension = \"\";\n            int index = fileName.lastIndexOf('.');\n            if (index > 0) {\n                extension = fileName.substring(index + 1);\n            }\n            BasicFileAttributes basicFileAttributes = getFileAttribute(path);\n\n            // prepare content path - remove root file path\n            String contentPath = path.getParent().toFile().toString();\n            int localFilePathIndex = contentPath.indexOf(localFilePath);\n            if (localFilePathIndex > -1) {\n                contentPath = contentPath.substring(localFilePathIndex);\n                contentPath = contentPath.replace(localFilePath, \"\");\n            }\n            Base64.Encoder encoder = Base64.getEncoder();\n            byte[] encodedByte = encoder.encode(basicFileAttributes.fileKey().toString().getBytes());\n            UUID uuid = UUID.nameUUIDFromBytes(encodedByte);\n\n            Content content = Content.builder().contentName(fileName).objectKey(uuid.toString()).contentPath(contentPath).contentType(extension).totalContentSize(basicFileAttributes.size()).build();\n            localFileMap.put(uuid.toString(), content);\n            return content;\n        }\n        return null;\n    }\n\n    /**\n     * Read basic file Attributes\n     *\n     * @param path Path\n     * @return BasicFileAttributes.\n     */\n    private BasicFileAttributes getFileAttribute(final Path path) {\n        try {\n            return Files.readAttributes(path, BasicFileAttributes.class);\n        } catch (IOException e) {\n            throw new RuntimeException(e);\n        }\n    }\n\n    /**\n     * Getting the size from the path.\n     *\n     * @param path Path.\n     * @return Long.\n     */\n    private Long sizeFromFile(Path path) {\n        try {\n            return Files.size(path);\n        } catch (IOException ioException) {\n            log.error(\"Error while getting the file size\", ioException);\n        }\n        return 0L;\n    }\n\n    /**\n     * Validate the content Request.\n     *\n     * @param contentRequest ContentRequest.\n     * @return boolean.\n     */\n    private boolean validateRequest(final ContentRequest contentRequest) {\n        if (contentRequest == null) {\n            throw new RuntimeException(\"video request object is empty\");\n        }\n\n        if (contentRequest.getRangeStart() > contentRequest.getRangeEnd()) {\n            return false;\n        }\n\n        return contentRequest.getFileName() != null && contentRequest.getFileType() != null;\n    }\n}\n"
  },
  {
    "path": "src/main/java/com/ask/home/videostream/config/ApplicationConfig.java",
    "content": "package com.ask.home.videostream.config;\n\nimport org.springframework.boot.autoconfigure.web.ServerProperties;\nimport org.springframework.context.annotation.Bean;\nimport org.springframework.context.annotation.Configuration;\nimport org.springframework.http.server.reactive.ServerHttpRequest;\nimport org.springframework.web.server.WebFilter;\n\n@Configuration\npublic class ApplicationConfig {\n\n    /**\n     * Method to enforce the context path webflux\n     * @param serverProperties ServerProperties\n     * @return WebFilter.\n     */\n    @Bean\n    public WebFilter contextPathWebFilter(ServerProperties serverProperties) {\n        String contextPath = serverProperties.getServlet().getContextPath();\n        return (exchange, chain) -> {\n            ServerHttpRequest request = exchange.getRequest();\n            if (request.getURI().getPath().startsWith(contextPath)) {\n                return chain.filter(\n                        exchange.mutate()\n                                .request(request.mutate().contextPath(contextPath).build())\n                                .build());\n            }\n            return chain.filter(exchange);\n        };\n    }\n}\n"
  },
  {
    "path": "src/main/java/com/ask/home/videostream/config/VideoStreamConfig.java",
    "content": "package com.ask.home.videostream.config;\n\n\nimport com.ask.home.videostream.adapter.ContentAdapter;\nimport com.ask.home.videostream.adapter.LocalFileContentAdapter;\nimport lombok.extern.slf4j.Slf4j;\nimport org.springframework.beans.factory.annotation.Value;\nimport org.springframework.context.annotation.Bean;\nimport org.springframework.context.annotation.Configuration;\n\n@Configuration\n@Slf4j\npublic class VideoStreamConfig {\n\n    @Bean\n    public ContentAdapter videoContentAdapter(@Value(\"${video.content.path}\") final String videoContentRootPath) {\n        log.info(\"video Content Path {}\", videoContentRootPath);\n        return new LocalFileContentAdapter(videoContentRootPath);\n    }\n}\n"
  },
  {
    "path": "src/main/java/com/ask/home/videostream/constants/ApplicationConstants.java",
    "content": "package com.ask.home.videostream.constants;\n\npublic class ApplicationConstants {\n    public static final String VIDEO = \"/video\";\n\n    public static final String CONTENT_TYPE = \"Content-Type\";\n    public static final String CONTENT_LENGTH = \"Content-Length\";\n    public static final String VIDEO_CONTENT = \"video/\";\n    public static final String CONTENT_RANGE = \"Content-Range\";\n    public static final String ACCEPT_RANGES = \"Accept-Ranges\";\n    public static final String BYTES = \"bytes\";\n    public static final int CHUNK_SIZE = 314700;\n    public static final int BYTE_RANGE = 1024;\n\n    private ApplicationConstants() {\n    }\n}\n"
  },
  {
    "path": "src/main/java/com/ask/home/videostream/controller/VideoController.java",
    "content": "package com.ask.home.videostream.controller;\n\nimport com.ask.home.videostream.model.Content;\nimport com.ask.home.videostream.service.VideoService;\nimport org.springframework.http.ResponseEntity;\nimport org.springframework.web.bind.annotation.*;\nimport reactor.core.publisher.Mono;\n\nimport java.util.List;\n\n@RestController\n@RequestMapping(\"/api/v1/videos\")\npublic class VideoController {\n\n    private final VideoService videoService;\n\n    public VideoController(VideoService videoService) {\n        this.videoService = videoService;\n    }\n\n    @GetMapping(\"/stream/{fileType}/{filePathAndName}\")\n    public Mono<ResponseEntity<byte[]>> streamVideoByPath(@RequestHeader(value = \"Range\", required = false) String httpRangeList, @PathVariable(\"fileType\") String fileType, @PathVariable(\"filePathAndName\") String filePathAndName) {\n        return Mono.just(videoService.prepareContentByFilePath(httpRangeList, filePathAndName, fileType));\n    }\n\n    @GetMapping(\"/stream/object-key/{objectKey}\")\n    public Mono<ResponseEntity<byte[]>> streamVideoByObjectKey(@RequestHeader(value = \"Range\", required = false) String httpRangeList, @PathVariable(\"objectKey\") String objectKey) {\n        return Mono.just(videoService.prepareContentByObjectKey(httpRangeList, objectKey));\n    }\n\n    @GetMapping\n    public Mono<ResponseEntity<List<Content>>> getAllContents() {\n\n        return Mono.just(videoService.getAllContents());\n    }\n}\n"
  },
  {
    "path": "src/main/java/com/ask/home/videostream/model/Content.java",
    "content": "package com.ask.home.videostream.model;\n\n\nimport com.fasterxml.jackson.annotation.JsonInclude;\nimport lombok.Builder;\nimport lombok.Data;\n\n\n/**\n * Contains the values of extracted video content and metadata\n */\n@Data\n@Builder\n@JsonInclude(JsonInclude.Include.NON_NULL)\npublic class Content {\n\n    private byte[] content;\n    private Long rangeStart;\n    private Long rangeEnd;\n    private Long contentLength;\n    private String contentPath;\n    private String contentType;\n    private String contentName;\n    private Long totalContentSize;\n    private String objectKey;\n}\n"
  },
  {
    "path": "src/main/java/com/ask/home/videostream/model/ContentRequest.java",
    "content": "package com.ask.home.videostream.model;\n\nimport lombok.Builder;\nimport lombok.Data;\n\n@Data\n@Builder\npublic class ContentRequest {\n\n    private String fileType;\n    private String fileName;\n    private long rangeStart;\n    private long rangeEnd;\n    private String filePath;\n}\n"
  },
  {
    "path": "src/main/java/com/ask/home/videostream/service/VideoService.java",
    "content": "package com.ask.home.videostream.service;\n\nimport com.ask.home.videostream.adapter.ContentAdapter;\nimport com.ask.home.videostream.model.Content;\nimport com.ask.home.videostream.model.ContentRequest;\nimport lombok.extern.slf4j.Slf4j;\nimport org.springframework.http.HttpStatus;\nimport org.springframework.http.ResponseEntity;\nimport org.springframework.stereotype.Service;\n\nimport java.util.Arrays;\nimport java.util.List;\nimport java.util.stream.Collectors;\n\nimport static com.ask.home.videostream.constants.ApplicationConstants.*;\n\n\n/**\n * Video Service that process the incoming request and extract the data out.\n */\n@Service\n@Slf4j\npublic class VideoService {\n\n    private static final String CONTENT_RANGE_FORMAT = \"%s %s-%s/%s\";\n    private final ContentAdapter videoContentAdapter;\n\n    public VideoService(final ContentAdapter videoContentAdapter) {\n        this.videoContentAdapter = videoContentAdapter;\n    }\n\n    /**\n     * Method to get the video data by the Object Key.\n     *\n     * @param range     Range of the content size.\n     * @param objectKey Object Key\n     * @return byte array of video with ResponseEntity.\n     */\n    public ResponseEntity<byte[]> prepareContentByObjectKey(final String range, final String objectKey) {\n        final Content content = videoContentAdapter.findFileByKey(objectKey);\n        if (content == null) {\n            return ResponseEntity.notFound().build();\n        }\n        final ContentRequest contentRequest = ContentRequest.builder().fileName(content.getContentName()).fileType(content.getContentType()).filePath(content.getContentPath()).build();\n        return prepareContent(range, contentRequest);\n    }\n\n    /**\n     * Get the Content by the path\n     *\n     * @param range           Range of the content size.\n     * @param filePathAndName relative path of the file and file name\n     * @param fileType        File Type\n     * @return byte array of video with ResponseEntity.\n     */\n    public ResponseEntity<byte[]> prepareContentByFilePath(final String range, final String filePathAndName, final String fileType) {\n        final String[] filePathAndNameSplit = filePathAndName.split(\"\\\\+\");\n        final String fileName = filePathAndNameSplit[filePathAndNameSplit.length - 1];\n        final String filePath = Arrays.stream(filePathAndNameSplit).limit(filePathAndNameSplit.length - 1).collect(Collectors.joining(\"/\"));\n        final String fileNameAndType = String.format(\"%s.%s\", fileName, fileType);\n\n        final ContentRequest contentRequest = ContentRequest.builder().fileName(fileNameAndType).fileType(fileType).filePath(filePath).build();\n        return prepareContent(range, contentRequest);\n    }\n\n    /**\n     * Get the content based on the request Object(ContentRequest)\n     *\n     * @param range          Range of the content size.\n     * @param contentRequest Content Data.\n     * @return byte array of video with ResponseEntity.\n     */\n    private ResponseEntity<byte[]> prepareContent(final String range, final ContentRequest contentRequest) {\n\n        try {\n\n            final Long fileSize = videoContentAdapter.getContentSize(contentRequest);\n            if (fileSize < 1) {\n                throw new RuntimeException(\"Not a valid file size\");\n            }\n\n            prepareContentRange(range, contentRequest);\n\n            final Content content = videoContentAdapter.getContent(contentRequest);\n            content.setContentType(contentRequest.getFileType());\n            content.setTotalContentSize(fileSize);\n            return prepareResponseEntity(content);\n        } catch (Exception exception) {\n            log.error(\"Exception while reading the file {}\", exception.getMessage());\n            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();\n        }\n    }\n\n    /**\n     * Prepare the response Entity\n     *\n     * @param content Content\n     * @return ResponseEntity\n     */\n    private ResponseEntity<byte[]> prepareResponseEntity(final Content content) {\n        HttpStatus httpStatus = HttpStatus.PARTIAL_CONTENT;\n        if (content.getRangeEnd() != null && content.getRangeEnd() >= content.getTotalContentSize()) {\n            httpStatus = HttpStatus.OK;\n        }\n\n        return ResponseEntity.status(httpStatus).header(CONTENT_TYPE, VIDEO_CONTENT + content.getContentType()).header(ACCEPT_RANGES, BYTES).header(CONTENT_LENGTH, String.valueOf(content.getContentLength())).header(CONTENT_RANGE, String.format(CONTENT_RANGE_FORMAT, BYTES, content.getRangeStart(), content.getRangeEnd(), content.getTotalContentSize())).body(content.getContent());\n    }\n\n    /**\n     * Prepare the request\n     *\n     * @param range          String.\n     * @param contentRequest ContentRequest.\n     */\n    private void prepareContentRange(final String range, final ContentRequest contentRequest) {\n        // if range doesn't present default to chunk size.\n        if (range == null) {\n            contentRequest.setRangeStart(0L);\n            contentRequest.setRangeEnd(CHUNK_SIZE);\n        } else {\n            //format Range: bytes=0-499\n            String[] ranges = range.split(\"-\");\n            long rangeStart = Long.parseLong(ranges[0].substring(6));\n            // default rangeEnd with chunk size\n            long rangeEnd = rangeStart + CHUNK_SIZE;\n\n            // if range end present in the request then pick from there\n            if (ranges.length > 1) {\n                rangeEnd = Long.parseLong(ranges[1]);\n            }\n\n            // Get the minimum of file size or rangeEnd.\n            rangeEnd = Math.min(rangeEnd, videoContentAdapter.getContentSize(contentRequest) - 1);\n            contentRequest.setRangeStart(rangeStart);\n            contentRequest.setRangeEnd(rangeEnd);\n        }\n    }\n\n    /**\n     * List Contents\n     *\n     * @return ResponseEntity<List < Content>>\n     */\n    public ResponseEntity<List<Content>> getAllContents() {\n        List<Content> contentList = videoContentAdapter.findAllContents();\n        if (contentList.isEmpty()) {\n            return ResponseEntity.noContent().build();\n        }\n        return ResponseEntity.ok(contentList);\n    }\n}"
  },
  {
    "path": "src/main/java/com/ask/home/videostream/util/FileUtil.java",
    "content": "package com.ask.home.videostream.util;\n\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport java.io.File;\nimport java.io.IOException;\nimport java.net.URL;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\n\nimport static com.ask.home.videostream.adapter.LocalFileContentAdapter.FILE_PATH_FORMAT;\nimport static com.ask.home.videostream.constants.ApplicationConstants.VIDEO;\npublic class FileUtil {\n\n    private static final Logger log = LoggerFactory.getLogger(FileUtil.class);\n\n    /**\n     * Get the filePath.\n     *\n     * @return String.\n     */\n    public static String getFilePath() {\n        URL url = FileUtil.class.getResource(VIDEO);\n        assert url != null;\n        return new File(url.getFile()).getAbsolutePath();\n    }\n\n    public static String getFilePath(final String basePath, String filePath, String fileName) {\n        String path;\n        if (filePath != null && filePath.trim().isEmpty()) {\n            path = String.format(FILE_PATH_FORMAT, basePath, fileName);\n        } else {\n            path = String.format(FILE_PATH_FORMAT,String.format(FILE_PATH_FORMAT, basePath, filePath), fileName);\n        }\n       return new File(path).getAbsolutePath();\n    }\n\n    /**\n     * Check the file is video.\n     * @param path Path\n     * @return boolean\n     */\n    public static boolean isVideoFile(Path path) {\n        try {\n            String contentType = Files.probeContentType(path);\n            return contentType != null && contentType.startsWith(\"video/\");\n        } catch (IOException ioException) {\n            log.error(\"Exception in when checking the file is video file {}\", ioException.getMessage());\n            return false;\n        }\n    }\n}\n"
  },
  {
    "path": "src/main/resources/application.yml",
    "content": "\nserver:\n  servlet:\n    context-path: /video-service\n  port: 8080\n\n\n\nvideo:\n  content:\n    path: ${VIDEO_CONTENT_PATH:target/classes/video}"
  },
  {
    "path": "src/test/java/com/ask/home/videostream/ApplicationTests.java",
    "content": "package com.ask.home.videostream;\n\nimport org.junit.jupiter.api.Assertions;\nimport org.junit.jupiter.api.Test;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.boot.test.context.SpringBootTest;\nimport org.springframework.context.ApplicationContext;\n\n@SpringBootTest\nclass ApplicationTests {\n\n\t@Autowired\n\tApplicationContext applicationContext;\n\n\t@Test\n\tvoid contextLoads() {\n\t\tAssertions.assertNotNull(applicationContext);\n\t}\n\n}\n"
  },
  {
    "path": "src/test/java/com/ask/home/videostream/adapter/LocalFileContentAdapterTest.java",
    "content": "package com.ask.home.videostream.adapter;\n\nimport com.ask.home.videostream.model.Content;\nimport com.ask.home.videostream.model.ContentRequest;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.extension.ExtendWith;\nimport org.mockito.junit.jupiter.MockitoExtension;\nimport org.springframework.test.util.ReflectionTestUtils;\n\nimport java.io.File;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\n\nimport static org.junit.jupiter.api.Assertions.*;\n\n@ExtendWith(MockitoExtension.class)\nclass LocalFileContentAdapterTest {\n\n    LocalFileContentAdapter localFileContentAdapter;\n\n    @BeforeEach\n    public void setup() {\n        ClassLoader classLoader = getClass().getClassLoader();\n        File file = new File(classLoader.getResource(\"video\").getFile());\n        localFileContentAdapter = new LocalFileContentAdapter(file.getAbsolutePath());\n    }\n\n    @Test\n    void findFileByKeyWithNullKey() {\n        assertThrows(RuntimeException.class, () -> localFileContentAdapter.findFileByKey(null));\n    }\n\n    @Test\n    void findFileByKeyWithInvalidKey() {\n        Content responseContent = localFileContentAdapter.findFileByKey(\"test\");\n        assertNull(responseContent);\n    }\n\n    @Test\n    void findFileByKeyWithValidKey() {\n        Map<String, Content> cache = new HashMap<>();\n        String fileKey = \"957e9073-9aec-3be2-a94e-268312e13bed\";\n        Content content = Content.builder().contentName(\"test\").objectKey(fileKey).build();\n        cache.put(fileKey, content);\n        ReflectionTestUtils.setField(localFileContentAdapter, \"localFileMap\", cache);\n        Content responseContent = localFileContentAdapter.findFileByKey(fileKey);\n\n        assertNotNull(responseContent);\n        assertNotNull(responseContent.getContentName());\n        assertNotNull(responseContent.getObjectKey());\n        assertEquals(fileKey, responseContent.getObjectKey());\n    }\n\n    @Test\n    void getContentWithEmptyData() {\n        ContentRequest contentRequest = ContentRequest.builder().fileName(\"video_empty.mp4\").fileType(\"mp4\").filePath(\"\").build();\n\n        Content content = localFileContentAdapter.getContent(contentRequest);\n        assertNotNull(content);\n        assertEquals(0, content.getContent().length);\n    }\n\n    @Test\n    void getContentWithRealVideoFile() {\n        ContentRequest contentRequest = ContentRequest.builder().fileName(\"toystory.mp4\").fileType(\"mp4\").filePath(\"\").build();\n\n        Content content = localFileContentAdapter.getContent(contentRequest);\n        assertNotNull(content);\n        assertTrue(content.getContent().length > 0);\n    }\n\n    @Test\n    void getContentWithNullObject() {\n        assertThrows(RuntimeException.class, () -> localFileContentAdapter.getContent(null));\n    }\n\n    @Test\n    void getContentSizeWithValidFile() {\n        ContentRequest contentRequest = ContentRequest.builder().fileName(\"toystory.mp4\").fileType(\"mp4\").filePath(\"\").build();\n\n        long contentSize = localFileContentAdapter.getContentSize(contentRequest);\n\n        assertEquals(33505479, contentSize);\n    }\n\n    @Test\n    void findAllContents() {\n        List<Content> contentList = localFileContentAdapter.findAllContents();\n        assertNotNull(contentList);\n        assertFalse(contentList.isEmpty());\n    }\n}"
  },
  {
    "path": "src/test/java/com/ask/home/videostream/controller/VideoControllerTest.java",
    "content": "package com.ask.home.videostream.controller;\n\nimport com.ask.home.videostream.service.VideoService;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.extension.ExtendWith;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.boot.test.autoconfigure.web.reactive.WebFluxTest;\nimport org.springframework.boot.test.mock.mockito.MockBean;\nimport org.springframework.http.ResponseEntity;\nimport org.springframework.test.context.junit.jupiter.SpringExtension;\nimport org.springframework.test.web.reactive.server.WebTestClient;\n\nimport java.util.Collections;\n\nimport static org.mockito.ArgumentMatchers.any;\nimport static org.mockito.Mockito.*;\n\n@WebFluxTest(controllers = VideoController.class)\n@ExtendWith(SpringExtension.class)\nclass VideoControllerTest {\n\n    @MockBean\n    VideoService videoService;\n\n    @Autowired\n    private WebTestClient webTestClient;\n\n    @Test\n    void streamVideoWithFilePathAndName() {\n        when(videoService.prepareContentByFilePath(any(), any(), any())).thenReturn(ResponseEntity.ok(new byte[]{}));\n        webTestClient.get().uri(\"/api/v1/videos/stream/mp4/toystory\").exchange().expectStatus().is2xxSuccessful();\n\n        verify(videoService, times(1)).prepareContentByFilePath(any(), any(), any());\n        verify(videoService, times(0)).prepareContentByObjectKey(any(), any());\n    }\n\n    @Test\n    void streamVideoWithFilePathFolderAndName() {\n        when(videoService.prepareContentByFilePath(any(), any(), any())).thenReturn(ResponseEntity.ok(new byte[]{}));\n        webTestClient.get().uri(\"/api/v1/videos/stream/mp4/video1+toystory\").exchange().expectStatus().is2xxSuccessful();\n\n        verify(videoService, times(1)).prepareContentByFilePath(any(), any(), any());\n        verify(videoService, times(0)).prepareContentByObjectKey(any(), any());\n    }\n\n    @Test\n    void testStreamVideoWithObjectKey() {\n        when(videoService.prepareContentByObjectKey(any(), any())).thenReturn(ResponseEntity.ok(new byte[]{}));\n        webTestClient.get().uri(\"/api/v1/videos/stream/object-key/test-key\").exchange().expectStatus().is2xxSuccessful();\n\n        verify(videoService, times(0)).prepareContentByFilePath(any(), any(), any());\n        verify(videoService, times(1)).prepareContentByObjectKey(any(), any());\n    }\n\n    @Test\n    void getAllContents() {\n        when(videoService.getAllContents()).thenReturn(ResponseEntity.ok(Collections.emptyList()));\n        webTestClient.get().uri(\"/api/v1/videos\").exchange().expectStatus().is2xxSuccessful();\n\n        verify(videoService, times(0)).prepareContentByFilePath(any(), any(), any());\n        verify(videoService, times(0)).prepareContentByObjectKey(any(), any());\n        verify(videoService, times(1)).getAllContents();\n    }\n}"
  },
  {
    "path": "src/test/java/com/ask/home/videostream/service/VideoServiceTest.java",
    "content": "package com.ask.home.videostream.service;\n\nimport com.ask.home.videostream.adapter.ContentAdapter;\nimport com.ask.home.videostream.model.Content;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.extension.ExtendWith;\nimport org.mockito.InjectMocks;\nimport org.mockito.Mock;\nimport org.mockito.Mockito;\nimport org.mockito.junit.jupiter.MockitoExtension;\nimport org.springframework.http.ResponseEntity;\n\nimport java.util.Collections;\nimport java.util.List;\n\nimport static org.junit.jupiter.api.Assertions.*;\nimport static org.mockito.ArgumentMatchers.any;\n\n@ExtendWith(MockitoExtension.class)\nclass VideoServiceTest {\n\n    @InjectMocks\n    VideoService videoService;\n\n    @Mock\n    ContentAdapter videoContentAdapter;\n\n\n    @Test\n    void prepareContentByObjectKeyWithValidObjectKey() {\n        Content content = Content.builder().contentPath(\"\").contentName(\"toystory.mp4\").content(new byte[]{}).build();\n        Mockito.when(videoContentAdapter.getContent(any())).thenReturn(content);\n        Mockito.when(videoContentAdapter.findFileByKey(any())).thenReturn(content);\n        Mockito.when(videoContentAdapter.getContentSize(any())).thenReturn(10L);\n\n        ResponseEntity<byte[]> responseEntity = videoService.prepareContentByObjectKey(\"bytes=0-\", \"test-key\");\n        assertNotNull(responseEntity);\n        assertTrue(responseEntity.getStatusCode().is2xxSuccessful());\n    }\n\n    @Test\n    void prepareContentByObjectKeyWithContentNotFound() {\n        Mockito.when(videoContentAdapter.findFileByKey(any())).thenReturn(null);\n\n        ResponseEntity<byte[]> responseEntity = videoService.prepareContentByObjectKey(\"bytes=0-\", \"test-key\");\n        assertNotNull(responseEntity);\n        assertTrue(responseEntity.getStatusCode().is4xxClientError());\n    }\n\n    @Test\n    void prepareContentByFilePath() {\n        Content content = Content.builder().contentPath(\"\").contentName(\"toystory.mp4\").content(new byte[]{}).build();\n        Mockito.when(videoContentAdapter.getContent(any())).thenReturn(content);\n        Mockito.when(videoContentAdapter.getContentSize(any())).thenReturn(10L);\n\n        ResponseEntity<byte[]> responseEntity = videoService.prepareContentByFilePath(\"bytes=0-\", \"toystory\", \"mp4\");\n        assertNotNull(responseEntity);\n        assertTrue(responseEntity.getStatusCode().is2xxSuccessful());\n    }\n\n    @Test\n    void prepareContentByFilePathWithoutRange() {\n        Content content = Content.builder().contentPath(\"\").contentName(\"toystory.mp4\").content(new byte[]{}).build();\n        Mockito.when(videoContentAdapter.getContent(any())).thenReturn(content);\n        Mockito.when(videoContentAdapter.getContentSize(any())).thenReturn(10L);\n\n        ResponseEntity<byte[]> responseEntity = videoService.prepareContentByFilePath(null, \"toystory\", \"mp4\");\n        assertNotNull(responseEntity);\n        assertTrue(responseEntity.getStatusCode().is2xxSuccessful());\n    }\n\n    @Test\n    void getAllContentsWithData() {\n        Content content = Content.builder().contentPath(\"\").contentName(\"toystory.mp4\").content(new byte[]{}).build();\n        Mockito.when(videoContentAdapter.findAllContents()).thenReturn(Collections.singletonList(content));\n\n        ResponseEntity<List<Content>> responseEntity = videoService.getAllContents();\n        assertNotNull(responseEntity);\n        assertTrue(responseEntity.getStatusCode().is2xxSuccessful());\n    }\n\n    @Test\n    void getAllContentsWithNoData() {\n        Mockito.when(videoContentAdapter.findAllContents()).thenReturn(Collections.emptyList());\n\n        ResponseEntity<List<Content>> responseEntity = videoService.getAllContents();\n        assertNotNull(responseEntity);\n        assertTrue(responseEntity.getStatusCode().is2xxSuccessful());\n        assertEquals(204, responseEntity.getStatusCode().value());\n    }\n}"
  },
  {
    "path": "src/test/resources/application-test.yml",
    "content": "\nserver:\n  servlet:\n    context-path: /video-service\n  port: 8080\n\n\n\nvideo:\n  content:\n    path: ${VIDEO_CONTENT_PATH:target/classes/video}"
  }
]