├── .github └── workflows │ └── Semgrep.yml ├── .gitignore ├── CODEOWNERS ├── CONTRIBUTING.md ├── README.md ├── build.gradle ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── src └── main │ ├── java │ └── com │ │ └── browserstack │ │ ├── gradle │ │ ├── AppAutomateUploadTask.java │ │ ├── AppLiveUploadTask.java │ │ ├── BrowserStackConfigExtension.java │ │ ├── BrowserStackPlugin.java │ │ ├── BrowserStackSDKPlugin.java │ │ ├── BrowserStackTask.java │ │ ├── CLI.java │ │ ├── Constants.java │ │ ├── EspressoTask.java │ │ └── Tools.java │ │ ├── httputils │ │ ├── HttpUtils.java │ │ ├── MultipartRequestComposer.java │ │ ├── OutputWriter.java │ │ ├── OutputWriterBundle.java │ │ ├── OutputWriterDataStream.java │ │ ├── OutputWriterDebug.java │ │ └── RequestBoundary.java │ │ └── json │ │ ├── CDL.java │ │ ├── Cookie.java │ │ ├── CookieList.java │ │ ├── HTTP.java │ │ ├── HTTPTokener.java │ │ ├── JSONArray.java │ │ ├── JSONException.java │ │ ├── JSONML.java │ │ ├── JSONObject.java │ │ ├── JSONPointer.java │ │ ├── JSONPointerException.java │ │ ├── JSONPropertyIgnore.java │ │ ├── JSONPropertyName.java │ │ ├── JSONString.java │ │ ├── JSONStringer.java │ │ ├── JSONTokener.java │ │ ├── JSONWriter.java │ │ ├── LICENSE │ │ ├── Property.java │ │ ├── README.md │ │ ├── XML.java │ │ └── XMLTokener.java │ └── resources │ └── META-INF │ └── gradle-plugins │ ├── com.browserstack.gradle-sdk.properties │ └── com.browserstack.gradle.properties └── test.rb /.github/workflows/Semgrep.yml: -------------------------------------------------------------------------------- 1 | # Name of this GitHub Actions workflow. 2 | name: Semgrep 3 | 4 | on: 5 | # Scan changed files in PRs (diff-aware scanning): 6 | # The branches below must be a subset of the branches above 7 | pull_request: 8 | branches: ["master", "main", "browserstack:master"] 9 | push: 10 | branches: ["master", "main", "browserstack:master"] 11 | schedule: 12 | - cron: '0 6 * * *' 13 | 14 | 15 | permissions: 16 | contents: read 17 | 18 | jobs: 19 | semgrep: 20 | # User definable name of this GitHub Actions job. 21 | permissions: 22 | contents: read # for actions/checkout to fetch code 23 | security-events: write # for github/codeql-action/upload-sarif to upload SARIF results 24 | name: semgrep/ci 25 | # If you are self-hosting, change the following `runs-on` value: 26 | runs-on: ubuntu-latest 27 | 28 | container: 29 | # A Docker image with Semgrep installed. Do not change this. 30 | image: returntocorp/semgrep 31 | 32 | # Skip any PR created by dependabot to avoid permission issues: 33 | if: (github.actor != 'dependabot[bot]') 34 | 35 | steps: 36 | # Fetch project source with GitHub Actions Checkout. 37 | - uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 38 | # Run the "semgrep ci" command on the command line of the docker image. 39 | - run: semgrep ci --sarif --output=semgrep.sarif 40 | env: 41 | # Add the rules that Semgrep uses by setting the SEMGREP_RULES environment variable. 42 | SEMGREP_RULES: p/default # more at semgrep.dev/explore 43 | 44 | - name: Upload SARIF file for GitHub Advanced Security Dashboard 45 | uses: github/codeql-action/upload-sarif@6c089f53dd51dc3fc7e599c3cb5356453a52ca9e # v2.20.0 46 | with: 47 | sarif_file: semgrep.sarif 48 | if: always() 49 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .gradle/ 2 | build/ 3 | .project 4 | .classpath 5 | .settings/ 6 | bin/ 7 | .idea/ 8 | out/ 9 | espresso-browserstack/ 10 | .java-version 11 | -------------------------------------------------------------------------------- /CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @browserstack/app-automate-public-repos 2 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to browserstack-gradle-plugin 2 | 3 | ### Requirements 4 | 5 | 1. Gradle - 3.0 6 | 2. Latest JDK 7 | 8 | ### Build plugin jar 9 | 10 | ``` 11 | ./gradlew clean build 12 | ``` 13 | 14 | You can see the jar in `builds/lib/` 15 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # browserstack-gradle-plugin 2 | 3 | This repository contains the source code for BrowserStack's Gradle plugin. 4 | 5 | 6 | ## PURPOSE 7 | 8 | This plugin consists of two types of tasks: 9 | 10 | 1. Builds, uploads and starts Espresso tests on BrowserStack AppAutomate. 11 | 2. Builds and uploads apk to BrowserStack AppLive for manual testing. 12 | 13 | ## USAGE 14 | 15 | ### Add to build.gradle 16 | 17 | #### Add plugin dependency to module's build.gradle 18 | 19 | ``` 20 | buildscript { 21 | repositories { 22 | maven { 23 | url "https://plugins.gradle.org/m2/" 24 | } 25 | } 26 | dependencies { 27 | classpath "gradle.plugin.com.browserstack.gradle:browserstack-gradle-plugin:3.1.0" 28 | } 29 | } 30 | 31 | apply plugin: "com.browserstack.gradle" 32 | ``` 33 | 34 | #### Add browserStackConfig parameters to module's build.gradle 35 | 36 | ``` 37 | browserStackConfig { 38 | username = "" 39 | accessKey = "" 40 | configFilePath = '' 41 | } 42 | ``` 43 | 44 | #### Sample Config file 45 | ``` 46 | { 47 | "devices": [ 48 | "Google Pixel-7.1" 49 | ], 50 | "deviceLogs": true, 51 | "networkLogs": true, 52 | "project": "Awesome gradle plugin build", 53 | "shards": { 54 | "numberOfShards": 3 55 | } 56 | } 57 | ``` 58 | > Note: To view the list of all supported parameters for Espresso tests on BrowserStack, visit complete list of API parameters section inside our [Espresso Get Started documentation](https://www.browserstack.com/app-automate/espresso/get-started) 59 | 60 | ### Tasks 61 | 62 | #### Espresso test task 63 | Builds, uploads and start Espresso tests on BrowserStack AppAutomate. 64 | 65 | ##### Gradle command 66 | 67 | gradle clean execute${buildVariantName}TestsOnBrowserstack 68 | 69 | For running tests on a project with no variants, you can simply run following command for building, uploading and running tests on debug apk: 70 | 71 | ``` 72 | gradle clean executeDebugTestsOnBrowserstack 73 | ``` 74 | 75 | And for projects with productFlavors, replace ${buildVariantName} with your build variant name, for example if your productFlavor name is "phone" and you want to test debug build type of this variant then command will be 76 | 77 | ``` 78 | gradle clean executePhoneDebugTestsOnBrowserstack 79 | 80 | ``` 81 | 82 | For running tests on a project without rebuilding apk and test suite, you can simply run following command for uploading and running tests on debug apk: 83 | 84 | ``` 85 | gradle executeDebugTestsOnBrowserstack -PskipBuildingApks=true 86 | ``` 87 | 88 | For specifying a config file from the command line, you can simply run the following command: 89 | 90 | ``` 91 | gradle clean executeDebugTestsOnBrowserstack --config-file='config-browserstack.json' 92 | ``` 93 | Note, this will override the entry within configFilePath. 94 | 95 | ##### Supported browserStackConfig parameters 96 | 97 | username: String 98 | accessKey: String 99 | configFilePath: String # Filepath that has capabilities specifed to run the build 100 | 101 | #### Browserstack CLI task 102 | Acts as a wrapper around the browserstack CLI and allows the operation of the CLI directly from gradle (Available from version 3.1.0) 103 | 104 | ##### Gradle command 105 | 106 | ` 107 | gradle browserstackCLIWrapper -Pcommand="browserstack-cli-command-goes-here" 108 | ` 109 | 110 | Example usage 111 | 112 | ``` 113 | gradle browserstackCLIWrapper -Pcommand="app-automate espresso run -a local-path-to-app-apk -t local-path-to-test-suite-apk" 114 | ``` 115 | 116 | You can refer to the existing browserstack CLI documentation [here](https://www.browserstack.com/app-automate/browserstack-cli) 117 | 118 | Any Browserstack CLI command can directly be passed to the -Pcommand parameter and it would execute the CLI command from gradle and push the output to stdout/terminal 119 | 120 | 121 | 122 | > Note: username, accessKey and configFilePath are mandatory parameters. Visit https://www.browserstack.com/app-automate/espresso/get-started to get started with Espresso Tests on BrowserStack and also to know more about the above mentioned parameters. 123 | 124 | > Note: List of supported devices and be found [here](https://api.browserstack.com/app-automate/espresso/devices.json) (basic auth required). For example :``` curl -u "$BROWSERSTACK_USERNAME:$BROWSERSTACK_ACCESS_KEY" https://api-cloud.browserstack.com/app-automate/devices.json ``` 125 | 126 | > Note: You can also set the values of username and accessKey in environment variables with names BROWSERSTACK_USERNAME and BROWSERSTACK_ACCESS_KEY, respectively. If you do this, then there is no need to set this parameters in browserStackConfig block. 127 | 128 | > Note: From version 3.1.0 onwards, usage of the Browserstack CLI task will download and install the latest version of Browserstack CLI on your machine. 129 | 130 | ##### Internal steps 131 | 132 | 1. Build debug and test apks, as dependencies are declared on `assemble${buildvariantName}` and `assemble${buildvariantName}AndroidTest` tasks. 133 | 2. Find the latest apks in the app directory recursively. 134 | 3. Upload both the apks on BrowserStack AppAutomate platform. 135 | 4. Execute Espresso test using the uploaded apps on the devices mentioned. 136 | 137 | #### Upload to AppLive task 138 | 139 | ##### Gradle command 140 | 141 | gradle clean upload${buildVariantName}ToBrowserstackAppLive 142 | 143 | For running tests on a project with no variants, you can simply run following command for uploading debug apk: 144 | 145 | ``` 146 | gradle clean uploadDebugToBrowserstackAppLive 147 | ``` 148 | 149 | And for projects with productFlavors, replace ${buildVariantName} with your build variant name, for example if your productFlavor name is "phone" and you want to upload debug build type of this variant then command will be gradle clean uploadPhoneDebugToBrowserstackAppLive. 150 | 151 | ##### Supported browserStackConfig parameters 152 | 153 | username: String 154 | accessKey: String 155 | 156 | > Note: username and accessKey are mandatory parameters. 157 | 158 | ##### Internal steps 159 | 160 | 1. Build debug and test apks, as dependencies are declared on `assemble${buildvariantName}` . 161 | 2. Find the latest apk in the app directory recursively. 162 | 3. Upload the apk on BrowserStack AppLive platform. 163 | 164 | 165 | > Note: You can also set the values of username and accessKey in environment variables with names BROWSERSTACK_USERNAME and BROWSERSTACK_ACCESS_KEY, respectively. If you do this, then there is no need to set these parameters in browserStackConfig block. 166 | 167 | > Note: You can also see all possible tasks by running "gradle tasks -all" 168 | 169 | ### Development 170 | 171 | Build the plugin 172 | 173 | ``` 174 | gradle clean build 175 | ``` 176 | 177 | To install the plugin into local maven repo 178 | 179 | ``` 180 | mvn install:install-file -Dfile=build/libs/browserstack-gradle-plugin-VERSION.jar -DgroupId=com.browserstack -DartifactId=gradle -Dversion=VERSION -Dpackaging=jar -DgeneratePom=true -DcreateChecksum=true 181 | ``` 182 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | 2 | plugins { 3 | id 'java-gradle-plugin' 4 | id 'com.gradle.plugin-publish' version '0.11.0' 5 | id 'maven-publish' 6 | } 7 | 8 | version '3.1.6' 9 | group 'com.browserstack' 10 | 11 | pluginBundle { 12 | website = 'https://www.browserstack.com' 13 | vcsUrl = 'https://github.com/browserstack/browserstack-gradle-plugin' 14 | 15 | plugins { 16 | browserstackPlugin { 17 | id = 'com.browserstack.gradle' 18 | displayName = 'BrowserStack\'s Gradle Plugin' 19 | description = 'Runs Espresso tests on BrowserStack' 20 | tags = ['espresso', 'test', 'browserstack', 'app', 'automate', 21 | 'app-automate', 'appautomate', 'app-live', 'applive'] 22 | version = '3.1.6' 23 | group = 'com.browserstack' 24 | } 25 | browserstackSDKPlugin { 26 | id = 'com.browserstack.gradle-sdk' 27 | displayName = 'BrowserStack SDK Gradle Solution' 28 | description = 'Cross browser testing for Gradle based projects with BrowserStack SDK' 29 | tags = ['test', 'browserstack'] 30 | version = '3.1.6' 31 | group = 'com.browserstack' 32 | } 33 | } 34 | } 35 | 36 | repositories { 37 | maven { url 'https://maven.google.com' } 38 | // google() 39 | jcenter() 40 | mavenCentral() 41 | } 42 | 43 | dependencies { 44 | // https://mvnrepository.com/artifact/com.android.tools.build/gradle 45 | implementation group: 'com.android.tools.build', name: 'gradle', version: '2.3.0' 46 | implementation group: 'com.googlecode.json-simple', name: 'json-simple', version: '1.1.1' 47 | } 48 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/browserstack/browserstack-gradle-plugin/9dc97ddaef77cdd4a50e02a2ddf0a8901dd7e01c/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-4.6-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Copyright © 2015-2021 the original authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | # 21 | # Gradle start up script for POSIX generated by Gradle. 22 | # 23 | # Important for running: 24 | # 25 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is 26 | # noncompliant, but you have some other compliant shell such as ksh or 27 | # bash, then to run this script, type that shell name before the whole 28 | # command line, like: 29 | # 30 | # ksh Gradle 31 | # 32 | # Busybox and similar reduced shells will NOT work, because this script 33 | # requires all of these POSIX shell features: 34 | # * functions; 35 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», 36 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»; 37 | # * compound commands having a testable exit status, especially «case»; 38 | # * various built-in commands including «command», «set», and «ulimit». 39 | # 40 | # Important for patching: 41 | # 42 | # (2) This script targets any POSIX shell, so it avoids extensions provided 43 | # by Bash, Ksh, etc; in particular arrays are avoided. 44 | # 45 | # The "traditional" practice of packing multiple parameters into a 46 | # space-separated string is a well documented source of bugs and security 47 | # problems, so this is (mostly) avoided, by progressively accumulating 48 | # options in "$@", and eventually passing that to Java. 49 | # 50 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, 51 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; 52 | # see the in-line comments for details. 53 | # 54 | # There are tweaks for specific operating systems such as AIX, CygWin, 55 | # Darwin, MinGW, and NonStop. 56 | # 57 | # (3) This script is generated from the Groovy template 58 | # https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt 59 | # within the Gradle project. 60 | # 61 | # You can find Gradle at https://github.com/gradle/gradle/. 62 | # 63 | ############################################################################## 64 | 65 | # Attempt to set APP_HOME 66 | 67 | # Resolve links: $0 may be a link 68 | app_path=$0 69 | 70 | # Need this for daisy-chained symlinks. 71 | while 72 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path 73 | [ -h "$app_path" ] 74 | do 75 | ls=$( ls -ld "$app_path" ) 76 | link=${ls#*' -> '} 77 | case $link in #( 78 | /*) app_path=$link ;; #( 79 | *) app_path=$APP_HOME$link ;; 80 | esac 81 | done 82 | 83 | APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit 84 | 85 | APP_NAME="Gradle" 86 | APP_BASE_NAME=${0##*/} 87 | 88 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 89 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 90 | 91 | # Use the maximum available, or set MAX_FD != -1 to use that value. 92 | MAX_FD=maximum 93 | 94 | warn () { 95 | echo "$*" 96 | } >&2 97 | 98 | die () { 99 | echo 100 | echo "$*" 101 | echo 102 | exit 1 103 | } >&2 104 | 105 | # OS specific support (must be 'true' or 'false'). 106 | cygwin=false 107 | msys=false 108 | darwin=false 109 | nonstop=false 110 | case "$( uname )" in #( 111 | CYGWIN* ) cygwin=true ;; #( 112 | Darwin* ) darwin=true ;; #( 113 | MSYS* | MINGW* ) msys=true ;; #( 114 | NONSTOP* ) nonstop=true ;; 115 | esac 116 | 117 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 118 | 119 | 120 | # Determine the Java command to use to start the JVM. 121 | if [ -n "$JAVA_HOME" ] ; then 122 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 123 | # IBM's JDK on AIX uses strange locations for the executables 124 | JAVACMD=$JAVA_HOME/jre/sh/java 125 | else 126 | JAVACMD=$JAVA_HOME/bin/java 127 | fi 128 | if [ ! -x "$JAVACMD" ] ; then 129 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 130 | 131 | Please set the JAVA_HOME variable in your environment to match the 132 | location of your Java installation." 133 | fi 134 | else 135 | JAVACMD=java 136 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 137 | 138 | Please set the JAVA_HOME variable in your environment to match the 139 | location of your Java installation." 140 | fi 141 | 142 | # Increase the maximum file descriptors if we can. 143 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then 144 | case $MAX_FD in #( 145 | max*) 146 | MAX_FD=$( ulimit -H -n ) || 147 | warn "Could not query maximum file descriptor limit" 148 | esac 149 | case $MAX_FD in #( 150 | '' | soft) :;; #( 151 | *) 152 | ulimit -n "$MAX_FD" || 153 | warn "Could not set maximum file descriptor limit to $MAX_FD" 154 | esac 155 | fi 156 | 157 | # Collect all arguments for the java command, stacking in reverse order: 158 | # * args from the command line 159 | # * the main class name 160 | # * -classpath 161 | # * -D...appname settings 162 | # * --module-path (only if needed) 163 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. 164 | 165 | # For Cygwin or MSYS, switch paths to Windows format before running java 166 | if "$cygwin" || "$msys" ; then 167 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) 168 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) 169 | 170 | JAVACMD=$( cygpath --unix "$JAVACMD" ) 171 | 172 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 173 | for arg do 174 | if 175 | case $arg in #( 176 | -*) false ;; # don't mess with options #( 177 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath 178 | [ -e "$t" ] ;; #( 179 | *) false ;; 180 | esac 181 | then 182 | arg=$( cygpath --path --ignore --mixed "$arg" ) 183 | fi 184 | # Roll the args list around exactly as many times as the number of 185 | # args, so each arg winds up back in the position where it started, but 186 | # possibly modified. 187 | # 188 | # NB: a `for` loop captures its iteration list before it begins, so 189 | # changing the positional parameters here affects neither the number of 190 | # iterations, nor the values presented in `arg`. 191 | shift # remove old arg 192 | set -- "$@" "$arg" # push replacement arg 193 | done 194 | fi 195 | 196 | # Collect all arguments for the java command; 197 | # * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of 198 | # shell script including quotes and variable substitutions, so put them in 199 | # double quotes to make sure that they get re-expanded; and 200 | # * put everything else in single quotes, so that it's not re-expanded. 201 | 202 | set -- \ 203 | "-Dorg.gradle.appname=$APP_BASE_NAME" \ 204 | -classpath "$CLASSPATH" \ 205 | org.gradle.wrapper.GradleWrapperMain \ 206 | "$@" 207 | 208 | # Use "xargs" to parse quoted args. 209 | # 210 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed. 211 | # 212 | # In Bash we could simply go: 213 | # 214 | # readarray ARGS < <( xargs -n1 <<<"$var" ) && 215 | # set -- "${ARGS[@]}" "$@" 216 | # 217 | # but POSIX shell has neither arrays nor command substitution, so instead we 218 | # post-process each arg (as a line of input to sed) to backslash-escape any 219 | # character that might be a shell metacharacter, then use eval to reverse 220 | # that process (while maintaining the separation between arguments), and wrap 221 | # the whole thing up as a single "set" statement. 222 | # 223 | # This will of course break if any of these variables contains a newline or 224 | # an unmatched quote. 225 | # 226 | 227 | eval "set -- $( 228 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | 229 | xargs -n1 | 230 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | 231 | tr '\n' ' ' 232 | )" '"$@"' 233 | 234 | exec "$JAVACMD" "$@" 235 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | -------------------------------------------------------------------------------- /src/main/java/com/browserstack/gradle/AppAutomateUploadTask.java: -------------------------------------------------------------------------------- 1 | package com.browserstack.gradle; 2 | 3 | import org.gradle.api.tasks.TaskAction; 4 | 5 | import java.io.IOException; 6 | import java.nio.file.Path; 7 | import java.util.Map; 8 | 9 | public class AppAutomateUploadTask extends BrowserStackTask { 10 | 11 | private void displayTestURL(String app_url) { 12 | String app_hashed_id = app_url.substring(5); 13 | System.out.println("Start testing at " + Constants.APP_AUTOMATE_HOST + "/#app_hashed_id=" + app_hashed_id); 14 | } 15 | 16 | public void verifyParams() throws Exception { 17 | String username = this.getUsername(); 18 | String accessKey = this.getAccessKey(); 19 | if (username == null || accessKey == null) { 20 | throw new Exception("`username`, `accessKey` are compulsory"); 21 | } 22 | } 23 | 24 | @TaskAction 25 | void upload() throws Exception { 26 | verifyParams(); 27 | final boolean ignoreTestPath = true; 28 | final boolean wrapPropsAsInternal = false; 29 | Map apkFiles = locateApks(ignoreTestPath); 30 | String app_url = uploadApp( 31 | wrapPropsAsInternal, 32 | Constants.APP_AUTOMATE_UPLOAD_PATH, 33 | apkFiles.get(BrowserStackTask.KEY_FILE_DEBUG) 34 | ); 35 | displayTestURL(app_url); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/main/java/com/browserstack/gradle/AppLiveUploadTask.java: -------------------------------------------------------------------------------- 1 | package com.browserstack.gradle; 2 | 3 | import org.gradle.api.tasks.TaskAction; 4 | import com.browserstack.gradle.Constants; 5 | import java.util.Map; 6 | import java.nio.file.Path; 7 | 8 | public class AppLiveUploadTask extends BrowserStackTask { 9 | 10 | private void displayTestURL(String app_url) { 11 | String app_hashed_id = app_url.substring(5); 12 | System.out.println("Start testing at " + Constants.APP_LIVE_HOST + "/#app_hashed_id=" + app_hashed_id); 13 | } 14 | 15 | public void verifyParams() throws Exception { 16 | String username = this.getUsername(); 17 | String accessKey = this.getAccessKey(); 18 | if (username == null || accessKey == null) { 19 | throw new Exception("`username`, `accessKey` are compulsory"); 20 | } 21 | } 22 | 23 | @TaskAction 24 | void uploadAndExecuteTest() throws Exception { 25 | verifyParams(); 26 | final boolean ignoreTestPath = true; 27 | final boolean wrapPropsAsInternal = true; 28 | Map apkFiles = locateApks(ignoreTestPath); 29 | String app_url = uploadApp( 30 | wrapPropsAsInternal, 31 | Constants.APP_LIVE_UPLOAD_PATH, 32 | apkFiles.get(BrowserStackTask.KEY_FILE_DEBUG) 33 | ); 34 | displayTestURL(app_url); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/main/java/com/browserstack/gradle/BrowserStackConfigExtension.java: -------------------------------------------------------------------------------- 1 | package com.browserstack.gradle; 2 | 3 | import java.util.HashMap; 4 | 5 | // This class is for getting browserstack configuration from gradle file. 6 | public class BrowserStackConfigExtension { 7 | 8 | private String username = System.getenv("BROWSERSTACK_USERNAME"); 9 | private String accessKey = System.getenv("BROWSERSTACK_ACCESS_KEY"); 10 | 11 | private String configFilePath; 12 | private String customId; 13 | 14 | /** 15 | * Enables debugging with more verbose logs 16 | */ 17 | private boolean isDebug = false; 18 | 19 | public String getUsername() { 20 | return username; 21 | } 22 | 23 | public String getAccessKey() { 24 | return accessKey; 25 | } 26 | 27 | public String getConfigFilePath() { 28 | return configFilePath; 29 | } 30 | 31 | public String getCustomId() { 32 | return customId; 33 | } 34 | 35 | public boolean isDebug() { 36 | return isDebug; 37 | } 38 | 39 | public void setUsername(String username) { 40 | this.username = username; 41 | } 42 | 43 | public void setAccessKey(String accessKey) { 44 | this.accessKey = accessKey; 45 | } 46 | 47 | public void setConfigFilePath(String filePath) { 48 | this.configFilePath = filePath; 49 | } 50 | 51 | public void setCustomId(String customId) { 52 | this.customId = customId; 53 | } 54 | 55 | public void setDebug(boolean debug) { 56 | isDebug = debug; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/main/java/com/browserstack/gradle/BrowserStackPlugin.java: -------------------------------------------------------------------------------- 1 | package com.browserstack.gradle; 2 | 3 | import com.android.build.gradle.AppExtension; 4 | import com.android.build.gradle.api.ApplicationVariant; 5 | import org.gradle.api.DomainObjectSet; 6 | import org.gradle.api.Plugin; 7 | import org.gradle.api.Project; 8 | 9 | import java.util.ArrayList; 10 | import java.util.List; 11 | 12 | public class BrowserStackPlugin implements Plugin { 13 | 14 | private static final String DEFAULT_GROUP = "BrowserStack"; 15 | 16 | public void apply(Project project) { 17 | 18 | BrowserStackConfigExtension browserStackConfigExtension = project.getExtensions() 19 | .create("browserStackConfig", BrowserStackConfigExtension.class); 20 | 21 | // Get android appExtension 22 | AppExtension appExtension = (AppExtension) project.getExtensions().getByName("android"); 23 | 24 | // Get all application variants or flavour combinations. 25 | DomainObjectSet appVariants = appExtension.getApplicationVariants(); 26 | 27 | // Create tasks for each variant 28 | final Boolean[] isCLITaskCreated = new Boolean[1]; 29 | isCLITaskCreated[0] = false; 30 | appVariants.all(applicationVariant -> { 31 | String applicationVariantName = null; 32 | try { 33 | applicationVariantName = Tools.capitalize(applicationVariant.getName()); 34 | } catch (Exception e) { 35 | return; 36 | } 37 | 38 | // Since we can't use an outer variable in lambda expression which is not final. 39 | final String appVariantName = applicationVariantName; 40 | project.getTasks().create("execute" + appVariantName + "TestsOnBrowserstack", EspressoTask.class, (task) -> { 41 | task.setGroup(DEFAULT_GROUP); 42 | task.setDescription("Uploads app / tests to AppAutomate and executes them"); 43 | // Run Espresso tests without building the apk and test apk 44 | if (!project.hasProperty("skipBuildingApks") || Boolean.parseBoolean(project.property("skipBuildingApks").toString()) == false) { 45 | task.dependsOn("assemble" + appVariantName, "assemble" + appVariantName + "AndroidTest"); 46 | } 47 | task.setAppVariantBaseName(applicationVariant.getBaseName()); 48 | task.setUsername(browserStackConfigExtension.getUsername()); 49 | task.setAccessKey(browserStackConfigExtension.getAccessKey()); 50 | task.setCustomId(browserStackConfigExtension.getCustomId()); 51 | task.setConfigFilePath(browserStackConfigExtension.getConfigFilePath()); 52 | task.setHost(Constants.BROWSERSTACK_API_HOST); 53 | task.setDebug(browserStackConfigExtension.isDebug()); 54 | if(project.hasProperty("mainAPKPath")){ 55 | task.setMainAPKPath(project.property("mainAPKPath").toString()); 56 | } 57 | if(project.hasProperty("testAPKPath")){ 58 | task.setTestAPKPath(project.property("testAPKPath").toString()); 59 | } 60 | }); 61 | 62 | project.getTasks().create("upload" + appVariantName + "ToBrowserstackAppLive", AppLiveUploadTask.class, (task) -> { 63 | task.setGroup(DEFAULT_GROUP); 64 | task.setDescription("Uploads app to AppLive"); 65 | task.dependsOn("assemble" + appVariantName); 66 | task.setAppVariantBaseName(applicationVariant.getBaseName()); 67 | task.setHost(Constants.BROWSERSTACK_API_HOST); 68 | task.setUsername(browserStackConfigExtension.getUsername()); 69 | task.setAccessKey(browserStackConfigExtension.getAccessKey()); 70 | task.setCustomId(browserStackConfigExtension.getCustomId()); 71 | task.setDebug(browserStackConfigExtension.isDebug()); 72 | }); 73 | 74 | project.getTasks().create("upload" + appVariantName + "ToBrowserstackAppAutomate", AppAutomateUploadTask.class, (task) -> { 75 | task.setGroup(DEFAULT_GROUP); 76 | task.setDescription("Uploads app to AppAutomate"); 77 | task.dependsOn("assemble" + appVariantName); 78 | task.setAppVariantBaseName(applicationVariant.getBaseName()); 79 | task.setHost(Constants.BROWSERSTACK_API_HOST); 80 | task.setUsername(browserStackConfigExtension.getUsername()); 81 | task.setAccessKey(browserStackConfigExtension.getAccessKey()); 82 | task.setCustomId(browserStackConfigExtension.getCustomId()); 83 | task.setDebug(browserStackConfigExtension.isDebug()); 84 | }); 85 | if (!isCLITaskCreated[0]) { 86 | project.getTasks().create("browserstackCLIWrapper", CLI.class, (task) -> { 87 | task.setGroup(DEFAULT_GROUP); 88 | task.setDescription("Just a wrapper on the Browserstack CLI. A way to run any Browserstack CLI command directly from gradle. \n" + 89 | "\n" + 90 | "\n" + 91 | "For reference on Browserstack CLI please visit https://www.browserstack.com/app-automate/browserstack-cli\n" + 92 | "\n" + 93 | "\n" + 94 | "Any CLI command passed in the custom option -Pcommand will be executed and the results will be displayed on the terminal.\n" + 95 | "\n" + 96 | "\n" + 97 | "For example:\n" + 98 | "\n" + 99 | "gradle browserstackCLIWrapper -Pcommand=”app-automate apps”\n" + 100 | "\n" + 101 | "The browserstack CLI command app-automate apps would run and the result will be displayed on the terminal. "); 102 | 103 | task.dependsOn("assemble" + appVariantName); 104 | task.setAppVariantBaseName(applicationVariant.getBaseName()); 105 | task.setHost(Constants.BROWSERSTACK_API_HOST); 106 | task.setUsername(browserStackConfigExtension.getUsername()); 107 | task.setAccessKey(browserStackConfigExtension.getAccessKey()); 108 | task.setCustomId(browserStackConfigExtension.getCustomId()); 109 | task.setDebug(browserStackConfigExtension.isDebug()); 110 | if (project.hasProperty("command")) { 111 | System.out.println("Command found: " + project.findProperty("command").toString()); 112 | task.setCommand(project.property("command").toString()); 113 | } 114 | }); 115 | isCLITaskCreated[0] = true; 116 | } 117 | }); 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /src/main/java/com/browserstack/gradle/BrowserStackSDKPlugin.java: -------------------------------------------------------------------------------- 1 | package com.browserstack.gradle; 2 | 3 | import com.browserstack.json.JSONObject; 4 | 5 | import java.io.*; 6 | 7 | import org.gradle.api.Plugin; 8 | import org.gradle.api.Project; 9 | import org.gradle.api.Task; 10 | import org.gradle.api.Action; 11 | import org.gradle.api.tasks.TaskContainer; 12 | import org.gradle.api.tasks.testing.Test; 13 | 14 | import org.gradle.api.execution.TaskExecutionGraph; 15 | 16 | public class BrowserStackSDKPlugin implements Plugin { 17 | String fileSeparator = System.getProperty("file.separator"); 18 | public void apply(Project project) { 19 | project.getTasks().withType(Task.class, task -> { 20 | task.doFirst(new Action() { 21 | @Override 22 | public void execute(Task t) { 23 | if(System.getenv("platformIndex") != null) return; 24 | 25 | org.gradle.StartParameter startParameter = project.getRootProject().getGradle().getStartParameter(); 26 | 27 | String fileSeparator = System.getProperty("file.separator"); 28 | String workingDirectoryPath = startParameter.getCurrentDir().toString(); 29 | String gradleConfigFile = "gradle-m-config.json"; 30 | String fullConfigFilePath = workingDirectoryPath + fileSeparator + gradleConfigFile; 31 | 32 | JSONObject jsonObject = new JSONObject(); 33 | jsonObject.put("projectDir", startParameter.getCurrentDir().toString()); 34 | 35 | org.gradle.TaskExecutionRequest taskExecutionRequest = startParameter.getTaskRequests().get(0); 36 | jsonObject.put("taskArgs", taskExecutionRequest.getArgs()); 37 | 38 | jsonObject.put("gradleHome", startParameter.DEFAULT_GRADLE_USER_HOME.toString()); 39 | jsonObject.put("logLevel", startParameter.getLogLevel().toString()); 40 | jsonObject.put("systemPropertiesArgs", startParameter.getSystemPropertiesArgs()); 41 | 42 | try (FileWriter newFile = new FileWriter(fullConfigFilePath)) { 43 | newFile.write(jsonObject.toString()); 44 | } catch (IOException e) { 45 | e.printStackTrace(); 46 | } 47 | } 48 | }); 49 | }); 50 | 51 | project.getGradle().getTaskGraph().whenReady(new Action() { 52 | @Override 53 | public void execute(TaskExecutionGraph taskGraph) { 54 | String customXmlResultsDir = project.getBuildDir().toString() + fileSeparator + "test-results-" + System.getenv("platformIndex"); 55 | if (customXmlResultsDir != null && !customXmlResultsDir.isEmpty()) { 56 | TaskContainer tasks = project.getTasks(); 57 | tasks.withType(Test.class, new Action() { 58 | @Override 59 | public void execute(Test test) { 60 | try { 61 | test.getReports().getJunitXml().setEnabled(false); 62 | test.getReports().getHtml().setEnabled(false); 63 | } catch (Throwable e) {} 64 | } 65 | }); 66 | } 67 | } 68 | }); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/main/java/com/browserstack/gradle/BrowserStackTask.java: -------------------------------------------------------------------------------- 1 | package com.browserstack.gradle; 2 | 3 | import com.browserstack.httputils.HttpUtils; 4 | import com.browserstack.json.JSONObject; 5 | 6 | import java.io.IOException; 7 | import java.net.HttpURLConnection; 8 | import java.nio.file.Files; 9 | import java.nio.file.NoSuchFileException; 10 | import java.nio.file.Path; 11 | import java.nio.file.Paths; 12 | import java.nio.file.attribute.BasicFileAttributes; 13 | import java.util.ArrayList; 14 | import java.util.Base64; 15 | import java.util.HashMap; 16 | import java.util.List; 17 | import java.util.Map; 18 | import org.gradle.api.DefaultTask; 19 | import org.gradle.api.tasks.Input; 20 | import org.jetbrains.annotations.NotNull; 21 | import org.gradle.api.tasks.Optional; 22 | 23 | 24 | public class BrowserStackTask extends DefaultTask { 25 | 26 | public static final String KEY_EXTRA_CUSTOM_ID = "custom_id"; 27 | public static final String KEY_FILE_DEBUG = "debugApkPath"; 28 | public static final String KEY_FILE_TEST = "testApkPath"; 29 | 30 | @Input 31 | protected String username, accessKey, customId; 32 | 33 | @Input 34 | private String app, host; 35 | 36 | protected boolean isDebug; 37 | 38 | private String appVariantBaseName = "debug"; 39 | 40 | @Input 41 | @Optional 42 | public String command ; 43 | 44 | @Input 45 | @Optional 46 | public String mainAPKPath; 47 | 48 | @Input 49 | @Optional 50 | public String testAPKPath; 51 | 52 | public void setAppVariantBaseName(String appVariantBaseName) { 53 | this.appVariantBaseName = appVariantBaseName; 54 | } 55 | 56 | public String getUsername() { 57 | return username; 58 | } 59 | 60 | public void setUsername(String username) { 61 | this.username = username; 62 | } 63 | 64 | public String getAccessKey() { 65 | return accessKey; 66 | } 67 | 68 | public void setAccessKey(String accessKey) { 69 | this.accessKey = accessKey; 70 | } 71 | 72 | public void setCustomId(String customId) { 73 | this.customId = customId; 74 | } 75 | 76 | public void setDebug(boolean debug) { 77 | isDebug = debug; 78 | } 79 | 80 | public String getHost() { 81 | return host; 82 | } 83 | 84 | public void setHost(String host) { 85 | this.host = host; 86 | } 87 | 88 | public String getCommand() { return command; } 89 | 90 | public void setCommand(String command) { this.command = command; } 91 | 92 | public String getMainAPKPath() { return mainAPKPath; } 93 | 94 | public void setMainAPKPath(String mainAPKPath) { this.mainAPKPath = mainAPKPath; } 95 | 96 | public String getTestAPKPath() {return testAPKPath; } 97 | 98 | public void setTestAPKPath(String testAPKPath) { this.testAPKPath = testAPKPath; } 99 | 100 | protected JSONObject constructDefaultBuildParams() { JSONObject params = new JSONObject(); 101 | 102 | params.put("app", app); 103 | // for monitoring, not for external use 104 | params.put("browserstack.source", "gradlePlugin"); 105 | 106 | return params; 107 | } 108 | 109 | /** 110 | * Uploads app and binds properties to it 111 | * @param wrapPropsAsInternal indicates if additional properties should be wrapped as internal data map 112 | * @param appUploadURLPath remote path to upload app to 113 | * @param debugApkPath app file path 114 | * @return raw request response 115 | * @throws IOException if uploading fails 116 | */ 117 | public String uploadApp( 118 | boolean wrapPropsAsInternal, 119 | @NotNull String appUploadURLPath, 120 | @NotNull Path debugApkPath 121 | ) throws IOException { 122 | try { 123 | final Map extraProperties = new HashMap<>(); 124 | extraProperties.put(KEY_EXTRA_CUSTOM_ID, this.customId); 125 | HttpURLConnection con = HttpUtils.sendPostApp( 126 | isDebug, 127 | wrapPropsAsInternal, 128 | host + appUploadURLPath, 129 | basicAuth(), 130 | debugApkPath.toString(), 131 | extraProperties 132 | ); 133 | int responseCode = con.getResponseCode(); 134 | System.out.println("App upload Response Code : " + responseCode); 135 | 136 | JSONObject response = new JSONObject(HttpUtils.getResponse(con, responseCode)); 137 | 138 | if (responseCode == 200) { 139 | app = (String) response.get("app_url"); 140 | return app; 141 | } else { 142 | throw new IOException( 143 | String.format( 144 | "App upload failed (%d): %s", 145 | responseCode, 146 | con.getResponseMessage() 147 | ) 148 | ); 149 | } 150 | } catch (IOException e) { 151 | // e.printStackTrace(); 152 | throw e; 153 | } 154 | } 155 | 156 | public String basicAuth() { 157 | return "Basic " + Base64.getEncoder().encodeToString((username + ":" + accessKey).getBytes()); 158 | } 159 | 160 | public static Path findMostRecentPath(List paths) { 161 | long mostRecentTime = 0L; 162 | Path mostRecentPath = null; 163 | for (Path p : paths) { 164 | if (p.toFile().lastModified() > mostRecentTime) { 165 | mostRecentTime = p.toFile().lastModified(); 166 | mostRecentPath = p; 167 | } 168 | } 169 | return mostRecentPath; 170 | } 171 | 172 | private boolean isPathRelative(String apkPath){ 173 | if(apkPath.startsWith("./")){ 174 | return true; 175 | } 176 | return false; 177 | } 178 | private String getAbsolutePath(String apkPath, String currentWorkingDirectory){ 179 | if(isPathRelative(apkPath)){ 180 | return currentWorkingDirectory + apkPath.substring(1); 181 | } 182 | return apkPath; 183 | } 184 | public Map locateApks(boolean ignoreTestPath) throws IOException { 185 | Path debugApkPath; 186 | Path testApkPath; 187 | String dir = System.getProperty("user.dir"); 188 | List appApkFiles = new ArrayList<>(); 189 | List testApkFiles = new ArrayList<>(); 190 | final Boolean[] isAPKFileCreated = {false,false}; // 1st element stores true if main apk is read from path provided by client and false otherwise. 2nd element is for test apk. 191 | if(mainAPKPath != null){ 192 | isAPKFileCreated[0] = true; 193 | try { 194 | Files.find(Paths.get(getAbsolutePath(mainAPKPath, dir)), 1, (filePath, fileAttr) -> isValidAPKFile(filePath, fileAttr)) 195 | .forEach(f -> { 196 | appApkFiles.add(f); 197 | }); 198 | }catch (NoSuchFileException e ){ 199 | throw new IOException("Invalid File Path: Please provide a valid main APK path"); 200 | } 201 | } 202 | if(testAPKPath != null){ 203 | isAPKFileCreated[1] = true; 204 | try { 205 | Files.find(Paths.get(getAbsolutePath(testAPKPath, dir)), 1, (filePath, fileAttr) -> isValidAPKFile(filePath, fileAttr)) 206 | .forEach(f -> { 207 | testApkFiles.add(f); 208 | }); 209 | }catch(NoSuchFileException e ){ 210 | throw new IOException("Invalid File Path: Please provide a valid test APK path"); 211 | } 212 | } 213 | 214 | if(!isAPKFileCreated[0] || !isAPKFileCreated[1]) { 215 | Files.find(Paths.get(dir), Constants.APP_SEARCH_MAX_DEPTH, (filePath, fileAttr) -> isValidFile(filePath, fileAttr)) 216 | .forEach(f -> { 217 | if (f.toString().endsWith("-androidTest.apk")) { 218 | if(!isAPKFileCreated[1]) { 219 | testApkFiles.add(f); 220 | } 221 | } else if (!isAPKFileCreated[0]) { 222 | appApkFiles.add(f); 223 | } 224 | }); 225 | } 226 | debugApkPath = findMostRecentPath(appApkFiles); 227 | testApkPath = findMostRecentPath(testApkFiles); 228 | 229 | System.out.println("Most recent DebugApp apk: " + debugApkPath); 230 | System.out.println("Most recent TestApp apk: " + testApkPath); 231 | 232 | if (debugApkPath == null) { 233 | throw new IOException("unable to find DebugApp apk"); 234 | } 235 | 236 | //Dont raise error for testApkPath if AppLive task 237 | if (!ignoreTestPath && testApkPath == null) { 238 | throw new IOException("unable to find TestApp apk"); 239 | } 240 | Map apkFiles = new HashMap<>(); 241 | apkFiles.put(KEY_FILE_DEBUG, debugApkPath); 242 | apkFiles.put(KEY_FILE_TEST, testApkPath); 243 | return apkFiles; 244 | } 245 | 246 | private boolean isValidFile(Path filePath, BasicFileAttributes fileAttr) { 247 | return isValidAPKFile(filePath, fileAttr) && filePath.getFileName().toString() 248 | .contains(appVariantBaseName); 249 | } 250 | private boolean isValidAPKFile(Path filePath, BasicFileAttributes fileAttr) { 251 | return fileAttr.isRegularFile() && filePath.toString().endsWith(".apk") ; 252 | } 253 | } 254 | -------------------------------------------------------------------------------- /src/main/java/com/browserstack/gradle/CLI.java: -------------------------------------------------------------------------------- 1 | package com.browserstack.gradle; 2 | 3 | import org.gradle.api.tasks.TaskAction; 4 | 5 | import java.io.File; 6 | import java.io.IOException; 7 | import java.util.ArrayList; 8 | import java.util.*; 9 | import java.net.*; 10 | import java.io.*; 11 | import java.util.Scanner; 12 | import java.util.regex.Pattern; 13 | import java.util.regex.Matcher; 14 | 15 | public class CLI extends BrowserStackTask { 16 | 17 | private Boolean isWindows; 18 | private String directory = System.getProperty("user.dir"); 19 | private String fileName = "browserstack"; 20 | private String downloadedFileName = "browserstackcli"; 21 | private String arch; 22 | private String os; 23 | 24 | public void verifyParams() throws Exception { 25 | String username = this.getUsername(); 26 | String accessKey = this.getAccessKey(); 27 | if (username == null || accessKey == null || username == "" || accessKey == "" || username.equals("null")) { 28 | throw new Exception("`username`, `accessKey` and `configFilePath` are compulsory"); 29 | } 30 | } 31 | 32 | private boolean initialize() { 33 | setOS(); 34 | isWindows = isWindows(); 35 | if (isWindows) { 36 | fileName = fileName + ".exe"; 37 | downloadedFileName = downloadedFileName + ".exe"; 38 | String wowArch = System.getenv("PROCESSOR_ARCHITECTURE"); 39 | String wow64Arch = System.getenv("PROCESSOR_ARCHITEW6432"); 40 | String realArch = wowArch != null && wowArch.endsWith("64") 41 | || wow64Arch != null && wow64Arch.endsWith("64") 42 | ? "64" : "32"; 43 | if (realArch.equals("64")) { 44 | arch = Constants.ARCH_64_BIT; 45 | } else { 46 | arch = Constants.ARCH_32_BIT; 47 | } 48 | } else { 49 | if (System.getProperty("os.arch").contains("64")) { 50 | arch = Constants.ARCH_64_BIT; 51 | } else { 52 | arch = Constants.ARCH_32_BIT; 53 | } 54 | } 55 | if (!new File(directory, downloadedFileName).exists()) { 56 | install(); 57 | try { 58 | givePermission(); 59 | } catch (InterruptedException e) { 60 | e.printStackTrace(); 61 | return false; 62 | } 63 | } 64 | return true; 65 | } 66 | 67 | private void setOS() { 68 | String osName = System.getProperty("os.name"); 69 | if (osName.toLowerCase().startsWith("windows")) { 70 | os = "windows"; 71 | } else if (osName.toLowerCase().startsWith("mac")) { 72 | os = "darwin"; 73 | } else { 74 | os = "linux"; 75 | } 76 | } 77 | 78 | @TaskAction 79 | void runCLICommands() throws Exception { 80 | verifyParams(); 81 | if (!initialize()) { 82 | System.out.println("Something went wrong!!"); 83 | return; 84 | } 85 | String s = ""; 86 | try { 87 | authenticate(); 88 | StringBuilder commandBuilder = getCommandPrefix(); 89 | commandBuilder.append(" ").append(command); 90 | String finalCommand = commandBuilder.toString(); 91 | Process process = runProcess(finalCommand); 92 | inheritIO(process.getInputStream(), System.out); 93 | inheritIO(process.getErrorStream(), System.err); 94 | process.waitFor(); 95 | } catch (Exception e) { 96 | e.printStackTrace(); 97 | } 98 | } 99 | 100 | private static void inheritIO(final InputStream src, final PrintStream dest) { 101 | new Thread(new Runnable() { 102 | public void run() { 103 | Scanner sc = new Scanner(src); 104 | while (sc.hasNextLine()) { 105 | dest.println(sc.nextLine()); 106 | } 107 | } 108 | }).start(); 109 | } 110 | 111 | private StringBuilder getCommandPrefix() { 112 | StringBuilder commandBuilder = new StringBuilder(""); 113 | if (isWindows) { 114 | commandBuilder.append(downloadedFileName); 115 | } else { 116 | commandBuilder.append("./"); 117 | commandBuilder.append(downloadedFileName); 118 | } 119 | return commandBuilder; 120 | } 121 | 122 | private void authenticate() throws InterruptedException { 123 | StringBuilder commandBuilder = getCommandPrefix(); 124 | commandBuilder.append(" authenticate --username=").append(this.getUsername()).append(" --access-key=").append(this.getAccessKey()); 125 | Process process = runProcess(commandBuilder.toString()); 126 | process.waitFor(); 127 | } 128 | 129 | private Process runProcess(String command) { 130 | Process process = null; 131 | ProcessBuilder builder; 132 | List list = new ArrayList<>(); 133 | Matcher matcher = Pattern.compile("([^\"]\\S*|\".+?\")\\s*").matcher(command); 134 | while (matcher.find()) 135 | list.add(matcher.group(1).replace("\"", "")); // Adding .replace("\"", "") to remove surrounding quotes. 136 | 137 | try { 138 | process = Runtime.getRuntime().exec(list.toArray(new String[0]), null, new File(directory)); 139 | } catch (IOException e) { 140 | e.printStackTrace(); 141 | System.exit(-1); 142 | } 143 | return process; 144 | } 145 | 146 | private Boolean isWindows() { 147 | return os.equals("windows"); 148 | } 149 | 150 | private void install() { 151 | try { 152 | String URL = generateDownloadURL(); 153 | URL url = new URL(URL); 154 | InputStream in = url.openStream(); 155 | FileOutputStream fos = new FileOutputStream(new File(directory + "/" + downloadedFileName)); 156 | 157 | int length = -1; 158 | byte[] buffer = new byte[1024];// buffer for portion of data from 159 | // connection 160 | while ((length = in.read(buffer)) > -1) { 161 | fos.write(buffer, 0, length); 162 | } 163 | fos.close(); 164 | in.close(); 165 | } catch (IOException e) { 166 | e.printStackTrace(); 167 | System.exit(-1); 168 | } 169 | } 170 | 171 | private String generateDownloadURL() { 172 | StringBuilder urlBuilder = new StringBuilder(Constants.SYNC_CLI_DOWNLOAD_URL); 173 | urlBuilder.append("arch="); 174 | urlBuilder.append(arch); 175 | urlBuilder.append("&file="); 176 | urlBuilder.append(fileName); 177 | urlBuilder.append("&os="); 178 | urlBuilder.append(os); 179 | urlBuilder.append("&version="); 180 | urlBuilder.append(Constants.SYNC_CLI_VERSION); 181 | return urlBuilder.toString(); 182 | } 183 | 184 | private void givePermission() throws InterruptedException { 185 | if (!isWindows) { 186 | Process process = runProcess("chmod +x " + downloadedFileName); 187 | process.waitFor(); 188 | } 189 | } 190 | } 191 | -------------------------------------------------------------------------------- /src/main/java/com/browserstack/gradle/Constants.java: -------------------------------------------------------------------------------- 1 | package com.browserstack.gradle; 2 | 3 | public class Constants { 4 | 5 | public static final String BROWSERSTACK_API_HOST = "https://api-cloud.browserstack.com", 6 | APP_LIVE_HOST = "https://app-live.browserstack.com", 7 | APP_AUTOMATE_HOST = "https://app-automate.browserstack.com", 8 | BUILD_PATH = "/app-automate/espresso/v2/build", 9 | APP_AUTOMATE_ESPRESSO_UPLOAD_PATH = "/app-automate/espresso/v2/app", 10 | APP_AUTOMATE_UPLOAD_PATH = "/app-automate/upload", 11 | APP_LIVE_UPLOAD_PATH = "/app-live/upload", 12 | TEST_SUITE_UPLOAD_PATH = "/app-automate/espresso/v2/test-suite", 13 | DEFAULT_NETWORK_PROFILE = null, 14 | SYNC_CLI_VERSION = "3.0.0", 15 | ARCH_64_BIT = "amd64", 16 | ARCH_32_BIT = "386", 17 | SYNC_CLI_DOWNLOAD_URL = "https://browserstack.com/browserstack-cli/download?"; 18 | 19 | public static final boolean DEFAULT_VIDEO = true, 20 | DEFAULT_DEVICE_LOGS = true, 21 | DEFAULT_NETWORK_LOGS = false, 22 | DEFAULT_LOCAL = false; 23 | 24 | public static final int APP_SEARCH_MAX_DEPTH = 10; 25 | 26 | } 27 | -------------------------------------------------------------------------------- /src/main/java/com/browserstack/gradle/EspressoTask.java: -------------------------------------------------------------------------------- 1 | package com.browserstack.gradle; 2 | 3 | import org.gradle.api.tasks.TaskAction; 4 | import java.io.FileReader; 5 | import java.net.HttpURLConnection; 6 | import java.util.HashMap; 7 | import java.util.Map; 8 | import java.nio.file.Path; 9 | import com.browserstack.json.JSONObject; 10 | import com.browserstack.httputils.HttpUtils; 11 | import org.json.simple.parser.JSONParser; 12 | import org.gradle.api.tasks.Input; 13 | import org.gradle.api.tasks.options.Option; 14 | 15 | public class EspressoTask extends BrowserStackTask { 16 | 17 | @Input 18 | private String configFilePath; 19 | 20 | private String testSuite; 21 | 22 | public void setConfigFilePath(String filePath) { 23 | this.configFilePath = filePath; 24 | } 25 | 26 | @Option(option = "config-file", description = "Config file passed through command line.") 27 | public void overrideConfigFilePath(String filePath) { 28 | this.configFilePath = filePath; 29 | } 30 | 31 | public String getConfigFilePath() { 32 | return configFilePath; 33 | } 34 | 35 | 36 | private String constructBuildParams() { 37 | JSONObject params = constructDefaultBuildParams(); 38 | 39 | JSONParser jsonParser = new JSONParser(); 40 | org.json.simple.JSONObject caps; 41 | 42 | try { 43 | Object obj = jsonParser.parse(new FileReader(getConfigFilePath())); 44 | org.json.simple.JSONObject jsonObject = (org.json.simple.JSONObject) obj; 45 | 46 | params.put("testSuite", testSuite); 47 | 48 | for (Object o : jsonObject.keySet()) { 49 | String key = (String) o; 50 | params.put(key, jsonObject.get(key)); 51 | } 52 | } catch (Exception e) { 53 | System.out.println("Config file parsing failed with below error: "); 54 | e.printStackTrace(); 55 | } 56 | 57 | return params.toString(); 58 | } 59 | 60 | private void uploadTestSuite(Path testApkPath) throws Exception { 61 | try { 62 | final boolean wrapPropsAsInternal = false; 63 | final Map extraProperties = new HashMap<>(); 64 | extraProperties.put(KEY_EXTRA_CUSTOM_ID, this.customId); 65 | HttpURLConnection con = HttpUtils.sendPostApp( 66 | isDebug, 67 | wrapPropsAsInternal, 68 | getHost() + Constants.TEST_SUITE_UPLOAD_PATH, 69 | basicAuth(), 70 | testApkPath.toString(), 71 | extraProperties 72 | ); 73 | int responseCode = con.getResponseCode(); 74 | System.out.println("TestSuite upload Response Code : " + responseCode); 75 | 76 | JSONObject response = new JSONObject(HttpUtils.getResponse(con, responseCode)); 77 | 78 | if (responseCode == 200) { 79 | testSuite = (String) response.get("test_suite_url"); 80 | } else { 81 | throw new Exception("TestSuite upload failed"); 82 | } 83 | 84 | } catch (Exception e) { 85 | e.printStackTrace(); 86 | throw e; 87 | } 88 | } 89 | 90 | private void executeTest() throws Exception { 91 | try { 92 | HttpURLConnection con = HttpUtils.sendPostBody( 93 | getHost() + Constants.BUILD_PATH, 94 | basicAuth(), 95 | constructBuildParams() 96 | ); 97 | int responseCode = con.getResponseCode(); 98 | System.out.println("Response Code : " + responseCode); 99 | 100 | JSONObject response = new JSONObject(HttpUtils.getResponse(con, responseCode)); 101 | 102 | if (responseCode == 200) { 103 | String build_id = response.getString("build_id"); 104 | displayDashboardURL(build_id); 105 | return; 106 | } 107 | 108 | return; 109 | 110 | } catch (Exception e) { 111 | e.printStackTrace(); 112 | throw e; 113 | } 114 | } 115 | 116 | private void displayDashboardURL(String build_id) { 117 | System.out.println("View build status at " + Constants.APP_AUTOMATE_HOST + "/builds/" + build_id); 118 | } 119 | 120 | public void verifyParams() throws Exception { 121 | String username = this.getUsername(); 122 | String accessKey = this.getAccessKey(); 123 | if (username == null || accessKey == null || configFilePath == null) { 124 | throw new Exception("`username`, `accessKey` and `configFilePath` are compulsory"); 125 | } 126 | } 127 | 128 | @TaskAction 129 | void uploadAndExecuteTest() throws Exception { 130 | verifyParams(); 131 | final boolean ignoreTestPath = false; 132 | final boolean wrapPropsAsInternal = false; 133 | Map apkFiles = locateApks(ignoreTestPath); 134 | uploadApp( 135 | wrapPropsAsInternal, 136 | Constants.APP_AUTOMATE_ESPRESSO_UPLOAD_PATH, 137 | apkFiles.get(BrowserStackTask.KEY_FILE_DEBUG) 138 | ); 139 | uploadTestSuite(apkFiles.get(BrowserStackTask.KEY_FILE_TEST)); 140 | executeTest(); 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /src/main/java/com/browserstack/gradle/Tools.java: -------------------------------------------------------------------------------- 1 | package com.browserstack.gradle; 2 | 3 | public class Tools { 4 | 5 | public static boolean isStringEmpty(String str) { 6 | return str == null || str.length() == 0; 7 | } 8 | 9 | public static String capitalize(String variantName) throws Exception { 10 | if (isStringEmpty(variantName)) { 11 | throw new Exception("Null or empty variantName passed."); 12 | } 13 | return variantName.substring(0, 1).toUpperCase() + variantName.substring(1); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/com/browserstack/httputils/HttpUtils.java: -------------------------------------------------------------------------------- 1 | package com.browserstack.httputils; 2 | 3 | import com.android.annotations.NonNull; 4 | import org.jetbrains.annotations.NotNull; 5 | import org.jetbrains.annotations.Nullable; 6 | 7 | import java.io.BufferedReader; 8 | import java.io.DataOutputStream; 9 | import java.io.IOException; 10 | import java.io.InputStreamReader; 11 | import java.net.HttpURLConnection; 12 | import java.net.URL; 13 | import java.util.Map; 14 | 15 | public class HttpUtils { 16 | 17 | /** 18 | * Uploads a file and binds custom data 19 | * @param isDebug enabled debugging logs when forming a request: 20 | * @param wrapPropsAsInternalDataMap indicates extra properties to be wrapped into internal data map 21 | * @param url endpoint url 22 | * @param authorization authorization token 23 | * @param appPath raw file path 24 | * @param properties extra properties bind to request 25 | * @return connection 26 | * @throws IOException error connecting / sending request 27 | */ 28 | public static HttpURLConnection sendPostApp( 29 | boolean isDebug, 30 | boolean wrapPropsAsInternalDataMap, 31 | @NotNull String url, 32 | @Nullable String authorization, 33 | @NotNull String appPath, 34 | @NonNull Map properties 35 | ) throws IOException { 36 | final OutputWriterDebug debugWriter = OutputWriterDebug.withDebugEnabled(isDebug); 37 | final RequestBoundary requestBoundary = RequestBoundary.generate(); 38 | URL obj = new URL(url); 39 | HttpURLConnection con = (HttpURLConnection) obj.openConnection(); 40 | con.setRequestMethod("POST"); 41 | if (authorization != null) { 42 | con.setRequestProperty("Authorization", authorization); 43 | } 44 | final String contentType = "multipart/form-data; boundary=" + requestBoundary.getBoundary(); 45 | con.setRequestProperty("Content-Type", contentType); 46 | con.setDoOutput(true); 47 | debugWriter.write(String.format("Request method: %s\n", con.getRequestMethod())); 48 | debugWriter.write(String.format("Request properties: %s\n", con.getRequestProperties())); 49 | DataOutputStream wr = new DataOutputStream(con.getOutputStream()); 50 | final MultipartRequestComposer.Builder multipartRequestBuilder = MultipartRequestComposer.Builder 51 | .newInstance(requestBoundary) 52 | .addWriter(new OutputWriterDataStream(wr)) 53 | .addWriter(debugWriter) 54 | .putFileFromPath(appPath) 55 | .putProperties(properties) 56 | .wrapProperiesAsInternalDataMap(wrapPropsAsInternalDataMap) 57 | ; 58 | multipartRequestBuilder 59 | .build() 60 | .write(); 61 | wr.flush(); 62 | wr.close(); 63 | return con; 64 | } 65 | 66 | public static HttpURLConnection sendPostBody( 67 | @NotNull String url, 68 | @Nullable String authorization, 69 | @NotNull String body 70 | ) throws Exception { 71 | URL obj = new URL(url); 72 | HttpURLConnection con = (HttpURLConnection) obj.openConnection(); 73 | con.setRequestMethod("POST"); 74 | 75 | if (authorization != null) { 76 | con.setRequestProperty("Authorization", authorization); 77 | } 78 | final String contentType = "application/json"; 79 | con.setRequestProperty("Content-Type", contentType); 80 | con.setDoOutput(true); 81 | DataOutputStream wr = new DataOutputStream(con.getOutputStream()); 82 | wr.writeBytes(body); 83 | wr.flush(); 84 | wr.close(); 85 | return con; 86 | } 87 | 88 | public static String getResponse(HttpURLConnection con, int responseCode) throws IOException { 89 | BufferedReader in = new BufferedReader(new InputStreamReader(responseCode == 200 ? con.getInputStream() : con.getErrorStream())); 90 | 91 | String inputLine; 92 | StringBuffer response = new StringBuffer(); 93 | 94 | while ((inputLine = in.readLine()) != null) { 95 | response.append(inputLine); 96 | } 97 | in.close(); 98 | 99 | System.out.println(response.toString()); 100 | return response.toString(); 101 | } 102 | } -------------------------------------------------------------------------------- /src/main/java/com/browserstack/httputils/MultipartRequestComposer.java: -------------------------------------------------------------------------------- 1 | package com.browserstack.httputils; 2 | 3 | import com.android.annotations.NonNull; 4 | import com.browserstack.json.JSONObject; 5 | import org.jetbrains.annotations.NotNull; 6 | import org.jetbrains.annotations.Nullable; 7 | 8 | import java.io.File; 9 | import java.io.IOException; 10 | import java.nio.file.Files; 11 | import java.nio.file.Path; 12 | import java.util.ArrayList; 13 | import java.util.HashMap; 14 | import java.util.List; 15 | import java.util.Map; 16 | 17 | /** 18 | * Generates a multipart request 19 | */ 20 | public class MultipartRequestComposer { 21 | 22 | private static final String KEY_FILE = "file"; 23 | private static final String KEY_DATA = "data"; 24 | 25 | @NotNull private final OutputWriter writer; 26 | @NotNull private final String boundary; 27 | @NotNull private final Map dataMap; 28 | 29 | public MultipartRequestComposer(@NotNull Builder builder) { 30 | this.writer = new OutputWriterBundle(builder.writers); 31 | this.boundary = builder.boundary.getBoundary(); 32 | this.dataMap = builder.dataMap; 33 | } 34 | 35 | private MultipartRequestComposer() { 36 | // Cannot build from empty constructor 37 | throw new IllegalArgumentException(); 38 | } 39 | 40 | public void write() throws IOException { 41 | writeRequest(dataMap); 42 | } 43 | 44 | private void writeRequest( 45 | @NotNull Map dataMap 46 | ) throws IOException { 47 | for (Map.Entry entry : dataMap.entrySet()) { 48 | writer.write(String.format("--%s\r\nContent-Disposition: form-data; ", boundary)); 49 | if (entry.getValue() instanceof File) { 50 | final File entryAsFile = ((File) entry.getValue()); 51 | final Path filePath = entryAsFile.toPath(); 52 | final String contentType = "application/octet-stream"; 53 | writer.write( 54 | String.format( 55 | "name=\"%s\"; filename=\"%s\"\r\nContent-Type: %s\r\n\r\n", 56 | entry.getKey(), 57 | entryAsFile.getName(), 58 | contentType 59 | ) 60 | ); 61 | writer.writeBytes(Files.readAllBytes(filePath)); 62 | writer.write("\r\n"); 63 | } 64 | if (entry.getValue() instanceof Map) { 65 | final Map entryAsDataMap = ((Map) entry.getValue()); 66 | final JSONObject internalEntriesAsJson = new JSONObject(); 67 | for (Map.Entry internalEntry : entryAsDataMap.entrySet()) { 68 | internalEntriesAsJson.put(internalEntry.getKey(), internalEntry.getValue()); 69 | } 70 | writer.write( 71 | String.format( 72 | "name=\"%s\"\r\n\r\n%s\r\n", 73 | entry.getKey(), 74 | internalEntriesAsJson.toString() 75 | ) 76 | ); 77 | } 78 | if (entry.getValue() instanceof String) { 79 | final String entryAsString = ((String) entry.getValue()); 80 | writer.write( 81 | String.format( 82 | "name=\"%s\"\r\n\r\n%s\r\n", 83 | entry.getKey(), 84 | entryAsString 85 | ) 86 | ); 87 | } 88 | } 89 | writer.write( 90 | String.format( 91 | "--%s--", 92 | boundary 93 | ) 94 | ); 95 | } 96 | 97 | @NotNull 98 | public String getBoundary() { 99 | return boundary; 100 | } 101 | 102 | public static class Builder { 103 | 104 | @NotNull private final RequestBoundary boundary; 105 | @NotNull private final List writers = new ArrayList<>(); 106 | @NotNull private final Map dataMap = new HashMap<>(); 107 | @NotNull private final Map propertyMap = new HashMap<>(); 108 | private boolean wrapProperiesAsInternalDataMap = false; 109 | 110 | public static Builder newInstance( 111 | @NotNull RequestBoundary requestBoundary 112 | ) { 113 | return new Builder( 114 | requestBoundary 115 | ); 116 | } 117 | 118 | private Builder(@NotNull RequestBoundary boundary) { 119 | this.boundary = boundary; 120 | } 121 | 122 | public Builder addWriter(@NotNull OutputWriter writer) { 123 | this.writers.add(writer); 124 | return this; 125 | } 126 | 127 | public Builder putFileFromPath(@NotNull String filePath) { 128 | final File file = new File(filePath); 129 | if (file.exists()) { 130 | this.dataMap.put(KEY_FILE, file); 131 | } 132 | return this; 133 | } 134 | 135 | public Builder putKeyValue( 136 | @NonNull String key, 137 | @Nullable String value 138 | ) { 139 | if (value != null) { 140 | this.propertyMap.put(key, value); 141 | } 142 | return this; 143 | } 144 | 145 | public Builder putProperties( 146 | @NonNull Map properties 147 | ) { 148 | for (Map.Entry entry : properties.entrySet()) { 149 | if (entry.getValue() != null) { 150 | this.propertyMap.put(entry.getKey(), entry.getValue()); 151 | } 152 | } 153 | return this; 154 | } 155 | 156 | /** 157 | * Indicates to either wrap all additional properties as an internal data map 158 | * or use as regular properties when forming a reuqest 159 | * @param isWrapEnabled is wrapping enabled 160 | * @return builder 161 | */ 162 | public Builder wrapProperiesAsInternalDataMap(boolean isWrapEnabled) { 163 | this.wrapProperiesAsInternalDataMap = isWrapEnabled; 164 | return this; 165 | } 166 | 167 | public MultipartRequestComposer build() { 168 | if (wrapProperiesAsInternalDataMap) { 169 | this.dataMap.put(KEY_DATA, this.propertyMap); 170 | } else { 171 | this.dataMap.putAll(propertyMap); 172 | } 173 | return new MultipartRequestComposer(this); 174 | } 175 | } 176 | } 177 | -------------------------------------------------------------------------------- /src/main/java/com/browserstack/httputils/OutputWriter.java: -------------------------------------------------------------------------------- 1 | package com.browserstack.httputils; 2 | 3 | import java.io.IOException; 4 | 5 | /** 6 | * Generic writer for output message piping 7 | */ 8 | public interface OutputWriter { 9 | void write(String message) throws IOException; 10 | void writeBytes(byte[] bytes) throws IOException; 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/com/browserstack/httputils/OutputWriterBundle.java: -------------------------------------------------------------------------------- 1 | package com.browserstack.httputils; 2 | 3 | import org.jetbrains.annotations.NotNull; 4 | 5 | import java.io.IOException; 6 | import java.util.Arrays; 7 | import java.util.List; 8 | 9 | /** 10 | * Bundles together multiple {@link OutputWriter} pipes 11 | */ 12 | public class OutputWriterBundle implements OutputWriter { 13 | 14 | @NotNull 15 | private final List writers; 16 | 17 | public OutputWriterBundle(@NotNull List writers) { 18 | this.writers = writers; 19 | } 20 | 21 | public OutputWriterBundle(@NotNull OutputWriter... writers) { 22 | this.writers = Arrays.asList(writers); 23 | } 24 | 25 | @Override 26 | public void write(String message) throws IOException { 27 | for (OutputWriter writer : writers) { 28 | writer.write(message); 29 | } 30 | } 31 | 32 | @Override 33 | public void writeBytes(byte[] bytes) throws IOException { 34 | for (OutputWriter writer : writers) { 35 | writer.writeBytes(bytes); 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/main/java/com/browserstack/httputils/OutputWriterDataStream.java: -------------------------------------------------------------------------------- 1 | package com.browserstack.httputils; 2 | 3 | import org.jetbrains.annotations.NotNull; 4 | 5 | import java.io.DataOutputStream; 6 | import java.io.IOException; 7 | import java.nio.charset.StandardCharsets; 8 | 9 | /** 10 | * Pipes messages to {@link DataOutputStream} 11 | */ 12 | public class OutputWriterDataStream implements OutputWriter { 13 | 14 | @NotNull 15 | private final DataOutputStream wr; 16 | 17 | public OutputWriterDataStream(@NotNull DataOutputStream wr) { 18 | this.wr = wr; 19 | } 20 | 21 | @Override 22 | public void write(String message) throws IOException { 23 | wr.write(message.getBytes(StandardCharsets.UTF_8)); 24 | } 25 | 26 | @Override 27 | public void writeBytes(byte[] bytes) throws IOException { 28 | wr.write(bytes); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/main/java/com/browserstack/httputils/OutputWriterDebug.java: -------------------------------------------------------------------------------- 1 | package com.browserstack.httputils; 2 | 3 | import java.io.IOException; 4 | 5 | /** 6 | * Pipes messages to System.out 7 | * Mainly for debug purposes 8 | */ 9 | public class OutputWriterDebug implements OutputWriter { 10 | 11 | private final boolean isEnabled; 12 | 13 | private OutputWriterDebug(boolean isEnabled) { 14 | this.isEnabled = isEnabled; 15 | } 16 | 17 | public static OutputWriterDebug withDebugEnabled(boolean isEnabled) { 18 | return new OutputWriterDebug(isEnabled); 19 | } 20 | 21 | @Override 22 | public void write(String message) throws IOException { 23 | if (isEnabled) { 24 | System.out.print(message); 25 | } 26 | } 27 | 28 | @Override 29 | public void writeBytes(byte[] bytes) throws IOException { 30 | if (isEnabled) { 31 | System.out.print("<---Raw bytes--->"); 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/main/java/com/browserstack/httputils/RequestBoundary.java: -------------------------------------------------------------------------------- 1 | package com.browserstack.httputils; 2 | 3 | import org.jetbrains.annotations.NotNull; 4 | 5 | import java.math.BigInteger; 6 | import java.util.Random; 7 | 8 | /** 9 | * Generates and wraps boundary 10 | * Useful for multipart request 11 | */ 12 | public class RequestBoundary { 13 | 14 | @NotNull 15 | private final String boundary; 16 | 17 | @NotNull 18 | public static RequestBoundary generate() { 19 | return new RequestBoundary( 20 | new BigInteger(35, new Random()).toString() 21 | ); 22 | } 23 | 24 | private RequestBoundary(@NotNull String boundary) { 25 | this.boundary = boundary; 26 | } 27 | 28 | @NotNull 29 | public String getBoundary() { 30 | return boundary; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/main/java/com/browserstack/json/CDL.java: -------------------------------------------------------------------------------- 1 | package com.browserstack.json; 2 | 3 | 4 | 5 | 6 | public class CDL { 7 | 8 | 9 | private static String getValue(JSONTokener x) throws JSONException { 10 | char c; 11 | char q; 12 | StringBuffer sb; 13 | do { 14 | c = x.next(); 15 | } while (c == ' ' || c == '\t'); 16 | switch (c) { 17 | case 0: 18 | return null; 19 | case '"': 20 | case '\'': 21 | q = c; 22 | sb = new StringBuffer(); 23 | for (;;) { 24 | c = x.next(); 25 | if (c == q) { 26 | 27 | char nextC = x.next(); 28 | if(nextC != '\"') { 29 | 30 | if(nextC > 0) { 31 | x.back(); 32 | } 33 | break; 34 | } 35 | } 36 | if (c == 0 || c == '\n' || c == '\r') { 37 | throw x.syntaxError("Missing close quote '" + q + "'."); 38 | } 39 | sb.append(c); 40 | } 41 | return sb.toString(); 42 | case ',': 43 | x.back(); 44 | return ""; 45 | default: 46 | x.back(); 47 | return x.nextTo(','); 48 | } 49 | } 50 | 51 | 52 | public static JSONArray rowToJSONArray(JSONTokener x) throws JSONException { 53 | JSONArray ja = new JSONArray(); 54 | for (;;) { 55 | String value = getValue(x); 56 | char c = x.next(); 57 | if (value == null || 58 | (ja.length() == 0 && value.length() == 0 && c != ',')) { 59 | return null; 60 | } 61 | ja.put(value); 62 | for (;;) { 63 | if (c == ',') { 64 | break; 65 | } 66 | if (c != ' ') { 67 | if (c == '\n' || c == '\r' || c == 0) { 68 | return ja; 69 | } 70 | throw x.syntaxError("Bad character '" + c + "' (" + 71 | (int)c + ")."); 72 | } 73 | c = x.next(); 74 | } 75 | } 76 | } 77 | 78 | 79 | public static JSONObject rowToJSONObject(JSONArray names, JSONTokener x) 80 | throws JSONException { 81 | JSONArray ja = rowToJSONArray(x); 82 | return ja != null ? ja.toJSONObject(names) : null; 83 | } 84 | 85 | 86 | public static String rowToString(JSONArray ja) { 87 | StringBuilder sb = new StringBuilder(); 88 | for (int i = 0; i < ja.length(); i += 1) { 89 | if (i > 0) { 90 | sb.append(','); 91 | } 92 | Object object = ja.opt(i); 93 | if (object != null) { 94 | String string = object.toString(); 95 | if (string.length() > 0 && (string.indexOf(',') >= 0 || 96 | string.indexOf('\n') >= 0 || string.indexOf('\r') >= 0 || 97 | string.indexOf(0) >= 0 || string.charAt(0) == '"')) { 98 | sb.append('"'); 99 | int length = string.length(); 100 | for (int j = 0; j < length; j += 1) { 101 | char c = string.charAt(j); 102 | if (c >= ' ' && c != '"') { 103 | sb.append(c); 104 | } 105 | } 106 | sb.append('"'); 107 | } else { 108 | sb.append(string); 109 | } 110 | } 111 | } 112 | sb.append('\n'); 113 | return sb.toString(); 114 | } 115 | 116 | 117 | public static JSONArray toJSONArray(String string) throws JSONException { 118 | return toJSONArray(new JSONTokener(string)); 119 | } 120 | 121 | 122 | public static JSONArray toJSONArray(JSONTokener x) throws JSONException { 123 | return toJSONArray(rowToJSONArray(x), x); 124 | } 125 | 126 | 127 | public static JSONArray toJSONArray(JSONArray names, String string) 128 | throws JSONException { 129 | return toJSONArray(names, new JSONTokener(string)); 130 | } 131 | 132 | 133 | public static JSONArray toJSONArray(JSONArray names, JSONTokener x) 134 | throws JSONException { 135 | if (names == null || names.length() == 0) { 136 | return null; 137 | } 138 | JSONArray ja = new JSONArray(); 139 | for (;;) { 140 | JSONObject jo = rowToJSONObject(names, x); 141 | if (jo == null) { 142 | break; 143 | } 144 | ja.put(jo); 145 | } 146 | if (ja.length() == 0) { 147 | return null; 148 | } 149 | return ja; 150 | } 151 | 152 | 153 | 154 | public static String toString(JSONArray ja) throws JSONException { 155 | JSONObject jo = ja.optJSONObject(0); 156 | if (jo != null) { 157 | JSONArray names = jo.names(); 158 | if (names != null) { 159 | return rowToString(names) + toString(names, ja); 160 | } 161 | } 162 | return null; 163 | } 164 | 165 | 166 | public static String toString(JSONArray names, JSONArray ja) 167 | throws JSONException { 168 | if (names == null || names.length() == 0) { 169 | return null; 170 | } 171 | StringBuffer sb = new StringBuffer(); 172 | for (int i = 0; i < ja.length(); i += 1) { 173 | JSONObject jo = ja.optJSONObject(i); 174 | if (jo != null) { 175 | sb.append(rowToString(jo.toJSONArray(names))); 176 | } 177 | } 178 | return sb.toString(); 179 | } 180 | } 181 | -------------------------------------------------------------------------------- /src/main/java/com/browserstack/json/Cookie.java: -------------------------------------------------------------------------------- 1 | package com.browserstack.json; 2 | 3 | 4 | 5 | 6 | public class Cookie { 7 | 8 | 9 | public static String escape(String string) { 10 | char c; 11 | String s = string.trim(); 12 | int length = s.length(); 13 | StringBuilder sb = new StringBuilder(length); 14 | for (int i = 0; i < length; i += 1) { 15 | c = s.charAt(i); 16 | if (c < ' ' || c == '+' || c == '%' || c == '=' || c == ';') { 17 | sb.append('%'); 18 | sb.append(Character.forDigit((char)((c >>> 4) & 0x0f), 16)); 19 | sb.append(Character.forDigit((char)(c & 0x0f), 16)); 20 | } else { 21 | sb.append(c); 22 | } 23 | } 24 | return sb.toString(); 25 | } 26 | 27 | 28 | 29 | public static JSONObject toJSONObject(String string) throws JSONException { 30 | String name; 31 | JSONObject jo = new JSONObject(); 32 | Object value; 33 | JSONTokener x = new JSONTokener(string); 34 | jo.put("name", x.nextTo('=')); 35 | x.next('='); 36 | jo.put("value", x.nextTo(';')); 37 | x.next(); 38 | while (x.more()) { 39 | name = unescape(x.nextTo("=;")); 40 | if (x.next() != '=') { 41 | if (name.equals("secure")) { 42 | value = Boolean.TRUE; 43 | } else { 44 | throw x.syntaxError("Missing '=' in cookie parameter."); 45 | } 46 | } else { 47 | value = unescape(x.nextTo(';')); 48 | x.next(); 49 | } 50 | jo.put(name, value); 51 | } 52 | return jo; 53 | } 54 | 55 | 56 | 57 | public static String toString(JSONObject jo) throws JSONException { 58 | StringBuilder sb = new StringBuilder(); 59 | 60 | sb.append(escape(jo.getString("name"))); 61 | sb.append("="); 62 | sb.append(escape(jo.getString("value"))); 63 | if (jo.has("expires")) { 64 | sb.append(";expires="); 65 | sb.append(jo.getString("expires")); 66 | } 67 | if (jo.has("domain")) { 68 | sb.append(";domain="); 69 | sb.append(escape(jo.getString("domain"))); 70 | } 71 | if (jo.has("path")) { 72 | sb.append(";path="); 73 | sb.append(escape(jo.getString("path"))); 74 | } 75 | if (jo.optBoolean("secure")) { 76 | sb.append(";secure"); 77 | } 78 | return sb.toString(); 79 | } 80 | 81 | 82 | public static String unescape(String string) { 83 | int length = string.length(); 84 | StringBuilder sb = new StringBuilder(length); 85 | for (int i = 0; i < length; ++i) { 86 | char c = string.charAt(i); 87 | if (c == '+') { 88 | c = ' '; 89 | } else if (c == '%' && i + 2 < length) { 90 | int d = JSONTokener.dehexchar(string.charAt(i + 1)); 91 | int e = JSONTokener.dehexchar(string.charAt(i + 2)); 92 | if (d >= 0 && e >= 0) { 93 | c = (char)(d * 16 + e); 94 | i += 2; 95 | } 96 | } 97 | sb.append(c); 98 | } 99 | return sb.toString(); 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/main/java/com/browserstack/json/CookieList.java: -------------------------------------------------------------------------------- 1 | package com.browserstack.json; 2 | 3 | 4 | 5 | 6 | public class CookieList { 7 | 8 | 9 | public static JSONObject toJSONObject(String string) throws JSONException { 10 | JSONObject jo = new JSONObject(); 11 | JSONTokener x = new JSONTokener(string); 12 | while (x.more()) { 13 | String name = Cookie.unescape(x.nextTo('=')); 14 | x.next('='); 15 | jo.put(name, Cookie.unescape(x.nextTo(';'))); 16 | x.next(); 17 | } 18 | return jo; 19 | } 20 | 21 | 22 | public static String toString(JSONObject jo) throws JSONException { 23 | boolean b = false; 24 | final StringBuilder sb = new StringBuilder(); 25 | 26 | for (final String key : jo.keySet()) { 27 | final Object value = jo.opt(key); 28 | if (!JSONObject.NULL.equals(value)) { 29 | if (b) { 30 | sb.append(';'); 31 | } 32 | sb.append(Cookie.escape(key)); 33 | sb.append("="); 34 | sb.append(Cookie.escape(value.toString())); 35 | b = true; 36 | } 37 | } 38 | return sb.toString(); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/main/java/com/browserstack/json/HTTP.java: -------------------------------------------------------------------------------- 1 | package com.browserstack.json; 2 | 3 | 4 | 5 | import java.util.Locale; 6 | 7 | 8 | public class HTTP { 9 | 10 | 11 | public static final String CRLF = "\r\n"; 12 | 13 | 14 | public static JSONObject toJSONObject(String string) throws JSONException { 15 | JSONObject jo = new JSONObject(); 16 | HTTPTokener x = new HTTPTokener(string); 17 | String token; 18 | 19 | token = x.nextToken(); 20 | if (token.toUpperCase(Locale.ROOT).startsWith("HTTP")) { 21 | 22 | // Response 23 | 24 | jo.put("HTTP-Version", token); 25 | jo.put("Status-Code", x.nextToken()); 26 | jo.put("Reason-Phrase", x.nextTo('\0')); 27 | x.next(); 28 | 29 | } else { 30 | 31 | // Request 32 | 33 | jo.put("Method", token); 34 | jo.put("Request-URI", x.nextToken()); 35 | jo.put("HTTP-Version", x.nextToken()); 36 | } 37 | 38 | // Fields 39 | 40 | while (x.more()) { 41 | String name = x.nextTo(':'); 42 | x.next(':'); 43 | jo.put(name, x.nextTo('\0')); 44 | x.next(); 45 | } 46 | return jo; 47 | } 48 | 49 | 50 | 51 | public static String toString(JSONObject jo) throws JSONException { 52 | StringBuilder sb = new StringBuilder(); 53 | if (jo.has("Status-Code") && jo.has("Reason-Phrase")) { 54 | sb.append(jo.getString("HTTP-Version")); 55 | sb.append(' '); 56 | sb.append(jo.getString("Status-Code")); 57 | sb.append(' '); 58 | sb.append(jo.getString("Reason-Phrase")); 59 | } else if (jo.has("Method") && jo.has("Request-URI")) { 60 | sb.append(jo.getString("Method")); 61 | sb.append(' '); 62 | sb.append('"'); 63 | sb.append(jo.getString("Request-URI")); 64 | sb.append('"'); 65 | sb.append(' '); 66 | sb.append(jo.getString("HTTP-Version")); 67 | } else { 68 | throw new JSONException("Not enough material for an HTTP header."); 69 | } 70 | sb.append(CRLF); 71 | 72 | for (final String key : jo.keySet()) { 73 | String value = jo.optString(key); 74 | if (!"HTTP-Version".equals(key) && !"Status-Code".equals(key) && 75 | !"Reason-Phrase".equals(key) && !"Method".equals(key) && 76 | !"Request-URI".equals(key) && !JSONObject.NULL.equals(value)) { 77 | sb.append(key); 78 | sb.append(": "); 79 | sb.append(jo.optString(key)); 80 | sb.append(CRLF); 81 | } 82 | } 83 | sb.append(CRLF); 84 | return sb.toString(); 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/main/java/com/browserstack/json/HTTPTokener.java: -------------------------------------------------------------------------------- 1 | package com.browserstack.json; 2 | 3 | 4 | 5 | 6 | public class HTTPTokener extends JSONTokener { 7 | 8 | 9 | public HTTPTokener(String string) { 10 | super(string); 11 | } 12 | 13 | 14 | 15 | public String nextToken() throws JSONException { 16 | char c; 17 | char q; 18 | StringBuilder sb = new StringBuilder(); 19 | do { 20 | c = next(); 21 | } while (Character.isWhitespace(c)); 22 | if (c == '"' || c == '\'') { 23 | q = c; 24 | for (;;) { 25 | c = next(); 26 | if (c < ' ') { 27 | throw syntaxError("Unterminated string."); 28 | } 29 | if (c == q) { 30 | return sb.toString(); 31 | } 32 | sb.append(c); 33 | } 34 | } 35 | for (;;) { 36 | if (c == 0 || Character.isWhitespace(c)) { 37 | return sb.toString(); 38 | } 39 | sb.append(c); 40 | c = next(); 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/main/java/com/browserstack/json/JSONArray.java: -------------------------------------------------------------------------------- 1 | package com.browserstack.json; 2 | 3 | 4 | 5 | import java.io.IOException; 6 | import java.io.StringWriter; 7 | import java.io.Writer; 8 | import java.lang.reflect.Array; 9 | import java.math.BigDecimal; 10 | import java.math.BigInteger; 11 | import java.util.ArrayList; 12 | import java.util.Collection; 13 | import java.util.Iterator; 14 | import java.util.List; 15 | import java.util.Map; 16 | 17 | 18 | 19 | public class JSONArray implements Iterable { 20 | 21 | 22 | private final ArrayList myArrayList; 23 | 24 | 25 | public JSONArray() { 26 | this.myArrayList = new ArrayList(); 27 | } 28 | 29 | 30 | public JSONArray(JSONTokener x) throws JSONException { 31 | this(); 32 | if (x.nextClean() != '[') { 33 | throw x.syntaxError("A JSONArray text must start with '['"); 34 | } 35 | 36 | char nextChar = x.nextClean(); 37 | if (nextChar == 0) { 38 | 39 | throw x.syntaxError("Expected a ',' or ']'"); 40 | } 41 | if (nextChar != ']') { 42 | x.back(); 43 | for (;;) { 44 | if (x.nextClean() == ',') { 45 | x.back(); 46 | this.myArrayList.add(JSONObject.NULL); 47 | } else { 48 | x.back(); 49 | this.myArrayList.add(x.nextValue()); 50 | } 51 | switch (x.nextClean()) { 52 | case 0: 53 | 54 | throw x.syntaxError("Expected a ',' or ']'"); 55 | case ',': 56 | nextChar = x.nextClean(); 57 | if (nextChar == 0) { 58 | 59 | throw x.syntaxError("Expected a ',' or ']'"); 60 | } 61 | if (nextChar == ']') { 62 | return; 63 | } 64 | x.back(); 65 | break; 66 | case ']': 67 | return; 68 | default: 69 | throw x.syntaxError("Expected a ',' or ']'"); 70 | } 71 | } 72 | } 73 | } 74 | 75 | 76 | public JSONArray(String source) throws JSONException { 77 | this(new JSONTokener(source)); 78 | } 79 | 80 | 81 | public JSONArray(Collection collection) { 82 | if (collection == null) { 83 | this.myArrayList = new ArrayList(); 84 | } else { 85 | this.myArrayList = new ArrayList(collection.size()); 86 | for (Object o: collection){ 87 | this.myArrayList.add(JSONObject.wrap(o)); 88 | } 89 | } 90 | } 91 | 92 | 93 | public JSONArray(Object array) throws JSONException { 94 | this(); 95 | if (array.getClass().isArray()) { 96 | int length = Array.getLength(array); 97 | this.myArrayList.ensureCapacity(length); 98 | for (int i = 0; i < length; i += 1) { 99 | this.put(JSONObject.wrap(Array.get(array, i))); 100 | } 101 | } else { 102 | throw new JSONException( 103 | "JSONArray initial value should be a string or collection or array."); 104 | } 105 | } 106 | 107 | @Override 108 | public Iterator iterator() { 109 | return this.myArrayList.iterator(); 110 | } 111 | 112 | 113 | public Object get(int index) throws JSONException { 114 | Object object = this.opt(index); 115 | if (object == null) { 116 | throw new JSONException("JSONArray[" + index + "] not found."); 117 | } 118 | return object; 119 | } 120 | 121 | 122 | public boolean getBoolean(int index) throws JSONException { 123 | Object object = this.get(index); 124 | if (object.equals(Boolean.FALSE) 125 | || (object instanceof String && ((String) object) 126 | .equalsIgnoreCase("false"))) { 127 | return false; 128 | } else if (object.equals(Boolean.TRUE) 129 | || (object instanceof String && ((String) object) 130 | .equalsIgnoreCase("true"))) { 131 | return true; 132 | } 133 | throw new JSONException("JSONArray[" + index + "] is not a boolean."); 134 | } 135 | 136 | 137 | public double getDouble(int index) throws JSONException { 138 | Object object = this.get(index); 139 | try { 140 | return object instanceof Number ? ((Number) object).doubleValue() 141 | : Double.parseDouble((String) object); 142 | } catch (Exception e) { 143 | throw new JSONException("JSONArray[" + index + "] is not a number.", e); 144 | } 145 | } 146 | 147 | 148 | public float getFloat(int index) throws JSONException { 149 | Object object = this.get(index); 150 | try { 151 | return object instanceof Number ? ((Number) object).floatValue() 152 | : Float.parseFloat(object.toString()); 153 | } catch (Exception e) { 154 | throw new JSONException("JSONArray[" + index 155 | + "] is not a number.", e); 156 | } 157 | } 158 | 159 | 160 | public Number getNumber(int index) throws JSONException { 161 | Object object = this.get(index); 162 | try { 163 | if (object instanceof Number) { 164 | return (Number)object; 165 | } 166 | return JSONObject.stringToNumber(object.toString()); 167 | } catch (Exception e) { 168 | throw new JSONException("JSONArray[" + index + "] is not a number.", e); 169 | } 170 | } 171 | 172 | 173 | public > E getEnum(Class clazz, int index) throws JSONException { 174 | E val = optEnum(clazz, index); 175 | if(val==null) { 176 | 177 | 178 | 179 | throw new JSONException("JSONArray[" + index + "] is not an enum of type " 180 | + JSONObject.quote(clazz.getSimpleName()) + "."); 181 | } 182 | return val; 183 | } 184 | 185 | 186 | public BigDecimal getBigDecimal (int index) throws JSONException { 187 | Object object = this.get(index); 188 | try { 189 | return new BigDecimal(object.toString()); 190 | } catch (Exception e) { 191 | throw new JSONException("JSONArray[" + index + 192 | "] could not convert to BigDecimal.", e); 193 | } 194 | } 195 | 196 | 197 | public BigInteger getBigInteger (int index) throws JSONException { 198 | Object object = this.get(index); 199 | try { 200 | return new BigInteger(object.toString()); 201 | } catch (Exception e) { 202 | throw new JSONException("JSONArray[" + index + 203 | "] could not convert to BigInteger.", e); 204 | } 205 | } 206 | 207 | 208 | public int getInt(int index) throws JSONException { 209 | Object object = this.get(index); 210 | try { 211 | return object instanceof Number ? ((Number) object).intValue() 212 | : Integer.parseInt((String) object); 213 | } catch (Exception e) { 214 | throw new JSONException("JSONArray[" + index + "] is not a number.", e); 215 | } 216 | } 217 | 218 | 219 | public JSONArray getJSONArray(int index) throws JSONException { 220 | Object object = this.get(index); 221 | if (object instanceof JSONArray) { 222 | return (JSONArray) object; 223 | } 224 | throw new JSONException("JSONArray[" + index + "] is not a JSONArray."); 225 | } 226 | 227 | 228 | public JSONObject getJSONObject(int index) throws JSONException { 229 | Object object = this.get(index); 230 | if (object instanceof JSONObject) { 231 | return (JSONObject) object; 232 | } 233 | throw new JSONException("JSONArray[" + index + "] is not a JSONObject."); 234 | } 235 | 236 | 237 | public long getLong(int index) throws JSONException { 238 | Object object = this.get(index); 239 | try { 240 | return object instanceof Number ? ((Number) object).longValue() 241 | : Long.parseLong((String) object); 242 | } catch (Exception e) { 243 | throw new JSONException("JSONArray[" + index + "] is not a number.", e); 244 | } 245 | } 246 | 247 | 248 | public String getString(int index) throws JSONException { 249 | Object object = this.get(index); 250 | if (object instanceof String) { 251 | return (String) object; 252 | } 253 | throw new JSONException("JSONArray[" + index + "] not a string."); 254 | } 255 | 256 | 257 | public boolean isNull(int index) { 258 | return JSONObject.NULL.equals(this.opt(index)); 259 | } 260 | 261 | 262 | public String join(String separator) throws JSONException { 263 | int len = this.length(); 264 | StringBuilder sb = new StringBuilder(); 265 | 266 | for (int i = 0; i < len; i += 1) { 267 | if (i > 0) { 268 | sb.append(separator); 269 | } 270 | sb.append(JSONObject.valueToString(this.myArrayList.get(i))); 271 | } 272 | return sb.toString(); 273 | } 274 | 275 | 276 | public int length() { 277 | return this.myArrayList.size(); 278 | } 279 | 280 | 281 | public Object opt(int index) { 282 | return (index < 0 || index >= this.length()) ? null : this.myArrayList 283 | .get(index); 284 | } 285 | 286 | 287 | public boolean optBoolean(int index) { 288 | return this.optBoolean(index, false); 289 | } 290 | 291 | 292 | public boolean optBoolean(int index, boolean defaultValue) { 293 | try { 294 | return this.getBoolean(index); 295 | } catch (Exception e) { 296 | return defaultValue; 297 | } 298 | } 299 | 300 | 301 | public double optDouble(int index) { 302 | return this.optDouble(index, Double.NaN); 303 | } 304 | 305 | 306 | public double optDouble(int index, double defaultValue) { 307 | Object val = this.opt(index); 308 | if (JSONObject.NULL.equals(val)) { 309 | return defaultValue; 310 | } 311 | if (val instanceof Number){ 312 | return ((Number) val).doubleValue(); 313 | } 314 | if (val instanceof String) { 315 | try { 316 | return Double.parseDouble((String) val); 317 | } catch (Exception e) { 318 | return defaultValue; 319 | } 320 | } 321 | return defaultValue; 322 | } 323 | 324 | 325 | public float optFloat(int index) { 326 | return this.optFloat(index, Float.NaN); 327 | } 328 | 329 | 330 | public float optFloat(int index, float defaultValue) { 331 | Object val = this.opt(index); 332 | if (JSONObject.NULL.equals(val)) { 333 | return defaultValue; 334 | } 335 | if (val instanceof Number){ 336 | return ((Number) val).floatValue(); 337 | } 338 | if (val instanceof String) { 339 | try { 340 | return Float.parseFloat((String) val); 341 | } catch (Exception e) { 342 | return defaultValue; 343 | } 344 | } 345 | return defaultValue; 346 | } 347 | 348 | 349 | public int optInt(int index) { 350 | return this.optInt(index, 0); 351 | } 352 | 353 | 354 | public int optInt(int index, int defaultValue) { 355 | Object val = this.opt(index); 356 | if (JSONObject.NULL.equals(val)) { 357 | return defaultValue; 358 | } 359 | if (val instanceof Number){ 360 | return ((Number) val).intValue(); 361 | } 362 | 363 | if (val instanceof String) { 364 | try { 365 | return new BigDecimal(val.toString()).intValue(); 366 | } catch (Exception e) { 367 | return defaultValue; 368 | } 369 | } 370 | return defaultValue; 371 | } 372 | 373 | 374 | public > E optEnum(Class clazz, int index) { 375 | return this.optEnum(clazz, index, null); 376 | } 377 | 378 | 379 | public > E optEnum(Class clazz, int index, E defaultValue) { 380 | try { 381 | Object val = this.opt(index); 382 | if (JSONObject.NULL.equals(val)) { 383 | return defaultValue; 384 | } 385 | if (clazz.isAssignableFrom(val.getClass())) { 386 | 387 | @SuppressWarnings("unchecked") 388 | E myE = (E) val; 389 | return myE; 390 | } 391 | return Enum.valueOf(clazz, val.toString()); 392 | } catch (IllegalArgumentException e) { 393 | return defaultValue; 394 | } catch (NullPointerException e) { 395 | return defaultValue; 396 | } 397 | } 398 | 399 | 400 | 401 | public BigInteger optBigInteger(int index, BigInteger defaultValue) { 402 | Object val = this.opt(index); 403 | if (JSONObject.NULL.equals(val)) { 404 | return defaultValue; 405 | } 406 | if (val instanceof BigInteger){ 407 | return (BigInteger) val; 408 | } 409 | if (val instanceof BigDecimal){ 410 | return ((BigDecimal) val).toBigInteger(); 411 | } 412 | if (val instanceof Double || val instanceof Float){ 413 | return new BigDecimal(((Number) val).doubleValue()).toBigInteger(); 414 | } 415 | if (val instanceof Long || val instanceof Integer 416 | || val instanceof Short || val instanceof Byte){ 417 | return BigInteger.valueOf(((Number) val).longValue()); 418 | } 419 | try { 420 | final String valStr = val.toString(); 421 | if(JSONObject.isDecimalNotation(valStr)) { 422 | return new BigDecimal(valStr).toBigInteger(); 423 | } 424 | return new BigInteger(valStr); 425 | } catch (Exception e) { 426 | return defaultValue; 427 | } 428 | } 429 | 430 | 431 | public BigDecimal optBigDecimal(int index, BigDecimal defaultValue) { 432 | Object val = this.opt(index); 433 | if (JSONObject.NULL.equals(val)) { 434 | return defaultValue; 435 | } 436 | if (val instanceof BigDecimal){ 437 | return (BigDecimal) val; 438 | } 439 | if (val instanceof BigInteger){ 440 | return new BigDecimal((BigInteger) val); 441 | } 442 | if (val instanceof Double || val instanceof Float){ 443 | return new BigDecimal(((Number) val).doubleValue()); 444 | } 445 | if (val instanceof Long || val instanceof Integer 446 | || val instanceof Short || val instanceof Byte){ 447 | return new BigDecimal(((Number) val).longValue()); 448 | } 449 | try { 450 | return new BigDecimal(val.toString()); 451 | } catch (Exception e) { 452 | return defaultValue; 453 | } 454 | } 455 | 456 | 457 | public JSONArray optJSONArray(int index) { 458 | Object o = this.opt(index); 459 | return o instanceof JSONArray ? (JSONArray) o : null; 460 | } 461 | 462 | 463 | public JSONObject optJSONObject(int index) { 464 | Object o = this.opt(index); 465 | return o instanceof JSONObject ? (JSONObject) o : null; 466 | } 467 | 468 | 469 | public long optLong(int index) { 470 | return this.optLong(index, 0); 471 | } 472 | 473 | 474 | public long optLong(int index, long defaultValue) { 475 | Object val = this.opt(index); 476 | if (JSONObject.NULL.equals(val)) { 477 | return defaultValue; 478 | } 479 | if (val instanceof Number){ 480 | return ((Number) val).longValue(); 481 | } 482 | 483 | if (val instanceof String) { 484 | try { 485 | return new BigDecimal(val.toString()).longValue(); 486 | } catch (Exception e) { 487 | return defaultValue; 488 | } 489 | } 490 | return defaultValue; 491 | } 492 | 493 | 494 | public Number optNumber(int index) { 495 | return this.optNumber(index, null); 496 | } 497 | 498 | 499 | public Number optNumber(int index, Number defaultValue) { 500 | Object val = this.opt(index); 501 | if (JSONObject.NULL.equals(val)) { 502 | return defaultValue; 503 | } 504 | if (val instanceof Number){ 505 | return (Number) val; 506 | } 507 | 508 | if (val instanceof String) { 509 | try { 510 | return JSONObject.stringToNumber((String) val); 511 | } catch (Exception e) { 512 | return defaultValue; 513 | } 514 | } 515 | return defaultValue; 516 | } 517 | 518 | 519 | public String optString(int index) { 520 | return this.optString(index, ""); 521 | } 522 | 523 | 524 | public String optString(int index, String defaultValue) { 525 | Object object = this.opt(index); 526 | return JSONObject.NULL.equals(object) ? defaultValue : object 527 | .toString(); 528 | } 529 | 530 | 531 | public JSONArray put(boolean value) { 532 | return this.put(value ? Boolean.TRUE : Boolean.FALSE); 533 | } 534 | 535 | 536 | public JSONArray put(Collection value) { 537 | return this.put(new JSONArray(value)); 538 | } 539 | 540 | 541 | public JSONArray put(double value) throws JSONException { 542 | return this.put(Double.valueOf(value)); 543 | } 544 | 545 | 546 | public JSONArray put(float value) throws JSONException { 547 | return this.put(Float.valueOf(value)); 548 | } 549 | 550 | 551 | public JSONArray put(int value) { 552 | return this.put(Integer.valueOf(value)); 553 | } 554 | 555 | 556 | public JSONArray put(long value) { 557 | return this.put(Long.valueOf(value)); 558 | } 559 | 560 | 561 | public JSONArray put(Map value) { 562 | return this.put(new JSONObject(value)); 563 | } 564 | 565 | 566 | public JSONArray put(Object value) { 567 | JSONObject.testValidity(value); 568 | this.myArrayList.add(value); 569 | return this; 570 | } 571 | 572 | 573 | public JSONArray put(int index, boolean value) throws JSONException { 574 | return this.put(index, value ? Boolean.TRUE : Boolean.FALSE); 575 | } 576 | 577 | 578 | public JSONArray put(int index, Collection value) throws JSONException { 579 | return this.put(index, new JSONArray(value)); 580 | } 581 | 582 | 583 | public JSONArray put(int index, double value) throws JSONException { 584 | return this.put(index, Double.valueOf(value)); 585 | } 586 | 587 | 588 | public JSONArray put(int index, float value) throws JSONException { 589 | return this.put(index, Float.valueOf(value)); 590 | } 591 | 592 | 593 | public JSONArray put(int index, int value) throws JSONException { 594 | return this.put(index, Integer.valueOf(value)); 595 | } 596 | 597 | 598 | public JSONArray put(int index, long value) throws JSONException { 599 | return this.put(index, Long.valueOf(value)); 600 | } 601 | 602 | 603 | public JSONArray put(int index, Map value) throws JSONException { 604 | this.put(index, new JSONObject(value)); 605 | return this; 606 | } 607 | 608 | 609 | public JSONArray put(int index, Object value) throws JSONException { 610 | if (index < 0) { 611 | throw new JSONException("JSONArray[" + index + "] not found."); 612 | } 613 | if (index < this.length()) { 614 | JSONObject.testValidity(value); 615 | this.myArrayList.set(index, value); 616 | return this; 617 | } 618 | if(index == this.length()){ 619 | 620 | return this.put(value); 621 | } 622 | 623 | 624 | this.myArrayList.ensureCapacity(index + 1); 625 | while (index != this.length()) { 626 | 627 | this.myArrayList.add(JSONObject.NULL); 628 | } 629 | return this.put(value); 630 | } 631 | 632 | 633 | public Object query(String jsonPointer) { 634 | return query(new JSONPointer(jsonPointer)); 635 | } 636 | 637 | 638 | public Object query(JSONPointer jsonPointer) { 639 | return jsonPointer.queryFrom(this); 640 | } 641 | 642 | 643 | public Object optQuery(String jsonPointer) { 644 | return optQuery(new JSONPointer(jsonPointer)); 645 | } 646 | 647 | 648 | public Object optQuery(JSONPointer jsonPointer) { 649 | try { 650 | return jsonPointer.queryFrom(this); 651 | } catch (JSONPointerException e) { 652 | return null; 653 | } 654 | } 655 | 656 | 657 | public Object remove(int index) { 658 | return index >= 0 && index < this.length() 659 | ? this.myArrayList.remove(index) 660 | : null; 661 | } 662 | 663 | 664 | public boolean similar(Object other) { 665 | if (!(other instanceof JSONArray)) { 666 | return false; 667 | } 668 | int len = this.length(); 669 | if (len != ((JSONArray)other).length()) { 670 | return false; 671 | } 672 | for (int i = 0; i < len; i += 1) { 673 | Object valueThis = this.myArrayList.get(i); 674 | Object valueOther = ((JSONArray)other).myArrayList.get(i); 675 | if(valueThis == valueOther) { 676 | continue; 677 | } 678 | if(valueThis == null) { 679 | return false; 680 | } 681 | if (valueThis instanceof JSONObject) { 682 | if (!((JSONObject)valueThis).similar(valueOther)) { 683 | return false; 684 | } 685 | } else if (valueThis instanceof JSONArray) { 686 | if (!((JSONArray)valueThis).similar(valueOther)) { 687 | return false; 688 | } 689 | } else if (!valueThis.equals(valueOther)) { 690 | return false; 691 | } 692 | } 693 | return true; 694 | } 695 | 696 | 697 | public JSONObject toJSONObject(JSONArray names) throws JSONException { 698 | if (names == null || names.length() == 0 || this.length() == 0) { 699 | return null; 700 | } 701 | JSONObject jo = new JSONObject(names.length()); 702 | for (int i = 0; i < names.length(); i += 1) { 703 | jo.put(names.getString(i), this.opt(i)); 704 | } 705 | return jo; 706 | } 707 | 708 | 709 | @Override 710 | public String toString() { 711 | try { 712 | return this.toString(0); 713 | } catch (Exception e) { 714 | return null; 715 | } 716 | } 717 | 718 | 719 | public String toString(int indentFactor) throws JSONException { 720 | StringWriter sw = new StringWriter(); 721 | synchronized (sw.getBuffer()) { 722 | return this.write(sw, indentFactor, 0).toString(); 723 | } 724 | } 725 | 726 | 727 | public Writer write(Writer writer) throws JSONException { 728 | return this.write(writer, 0, 0); 729 | } 730 | 731 | 732 | public Writer write(Writer writer, int indentFactor, int indent) 733 | throws JSONException { 734 | try { 735 | boolean commanate = false; 736 | int length = this.length(); 737 | writer.write('['); 738 | 739 | if (length == 1) { 740 | try { 741 | JSONObject.writeValue(writer, this.myArrayList.get(0), 742 | indentFactor, indent); 743 | } catch (Exception e) { 744 | throw new JSONException("Unable to write JSONArray value at index: 0", e); 745 | } 746 | } else if (length != 0) { 747 | final int newindent = indent + indentFactor; 748 | 749 | for (int i = 0; i < length; i += 1) { 750 | if (commanate) { 751 | writer.write(','); 752 | } 753 | if (indentFactor > 0) { 754 | writer.write('\n'); 755 | } 756 | JSONObject.indent(writer, newindent); 757 | try { 758 | JSONObject.writeValue(writer, this.myArrayList.get(i), 759 | indentFactor, newindent); 760 | } catch (Exception e) { 761 | throw new JSONException("Unable to write JSONArray value at index: " + i, e); 762 | } 763 | commanate = true; 764 | } 765 | if (indentFactor > 0) { 766 | writer.write('\n'); 767 | } 768 | JSONObject.indent(writer, indent); 769 | } 770 | writer.write(']'); 771 | return writer; 772 | } catch (IOException e) { 773 | throw new JSONException(e); 774 | } 775 | } 776 | 777 | 778 | public List toList() { 779 | List results = new ArrayList(this.myArrayList.size()); 780 | for (Object element : this.myArrayList) { 781 | if (element == null || JSONObject.NULL.equals(element)) { 782 | results.add(null); 783 | } else if (element instanceof JSONArray) { 784 | results.add(((JSONArray) element).toList()); 785 | } else if (element instanceof JSONObject) { 786 | results.add(((JSONObject) element).toMap()); 787 | } else { 788 | results.add(element); 789 | } 790 | } 791 | return results; 792 | } 793 | } 794 | -------------------------------------------------------------------------------- /src/main/java/com/browserstack/json/JSONException.java: -------------------------------------------------------------------------------- 1 | package com.browserstack.json; 2 | 3 | 4 | public class JSONException extends RuntimeException { 5 | 6 | private static final long serialVersionUID = 0; 7 | 8 | 9 | public JSONException(final String message) { 10 | super(message); 11 | } 12 | 13 | 14 | public JSONException(final String message, final Throwable cause) { 15 | super(message, cause); 16 | } 17 | 18 | 19 | public JSONException(final Throwable cause) { 20 | super(cause.getMessage(), cause); 21 | } 22 | 23 | } 24 | -------------------------------------------------------------------------------- /src/main/java/com/browserstack/json/JSONML.java: -------------------------------------------------------------------------------- 1 | package com.browserstack.json; 2 | 3 | 4 | 5 | 6 | public class JSONML { 7 | 8 | private static Object parse( 9 | XMLTokener x, 10 | boolean arrayForm, 11 | JSONArray ja, 12 | boolean keepStrings 13 | ) throws JSONException { 14 | String attribute; 15 | char c; 16 | String closeTag = null; 17 | int i; 18 | JSONArray newja = null; 19 | JSONObject newjo = null; 20 | Object token; 21 | String tagName = null; 22 | 23 | // Test for and skip past these forms: 24 | // 25 | // 26 | // 27 | // 28 | 29 | while (true) { 30 | if (!x.more()) { 31 | throw x.syntaxError("Bad XML"); 32 | } 33 | token = x.nextContent(); 34 | if (token == XML.LT) { 35 | token = x.nextToken(); 36 | if (token instanceof Character) { 37 | if (token == XML.SLASH) { 38 | 39 | // Close tag "); 59 | } else { 60 | x.back(); 61 | } 62 | } else if (c == '[') { 63 | token = x.nextToken(); 64 | if (token.equals("CDATA") && x.next() == '[') { 65 | if (ja != null) { 66 | ja.put(x.nextCDATA()); 67 | } 68 | } else { 69 | throw x.syntaxError("Expected 'CDATA['"); 70 | } 71 | } else { 72 | i = 1; 73 | do { 74 | token = x.nextMeta(); 75 | if (token == null) { 76 | throw x.syntaxError("Missing '>' after ' 0); 83 | } 84 | } else if (token == XML.QUEST) { 85 | 86 | // "); 89 | } else { 90 | throw x.syntaxError("Misshaped tag"); 91 | } 92 | 93 | // Open tag < 94 | 95 | } else { 96 | if (!(token instanceof String)) { 97 | throw x.syntaxError("Bad tagName '" + token + "'."); 98 | } 99 | tagName = (String)token; 100 | newja = new JSONArray(); 101 | newjo = new JSONObject(); 102 | if (arrayForm) { 103 | newja.put(tagName); 104 | if (ja != null) { 105 | ja.put(newja); 106 | } 107 | } else { 108 | newjo.put("tagName", tagName); 109 | if (ja != null) { 110 | ja.put(newjo); 111 | } 112 | } 113 | token = null; 114 | for (;;) { 115 | if (token == null) { 116 | token = x.nextToken(); 117 | } 118 | if (token == null) { 119 | throw x.syntaxError("Misshaped tag"); 120 | } 121 | if (!(token instanceof String)) { 122 | break; 123 | } 124 | 125 | // attribute = value 126 | 127 | attribute = (String)token; 128 | if (!arrayForm && ("tagName".equals(attribute) || "childNode".equals(attribute))) { 129 | throw x.syntaxError("Reserved attribute."); 130 | } 131 | token = x.nextToken(); 132 | if (token == XML.EQ) { 133 | token = x.nextToken(); 134 | if (!(token instanceof String)) { 135 | throw x.syntaxError("Missing value"); 136 | } 137 | newjo.accumulate(attribute, keepStrings ? ((String)token) :XML.stringToValue((String)token)); 138 | token = null; 139 | } else { 140 | newjo.accumulate(attribute, ""); 141 | } 142 | } 143 | if (arrayForm && newjo.length() > 0) { 144 | newja.put(newjo); 145 | } 146 | 147 | // Empty tag <.../> 148 | 149 | if (token == XML.SLASH) { 150 | if (x.nextToken() != XML.GT) { 151 | throw x.syntaxError("Misshaped tag"); 152 | } 153 | if (ja == null) { 154 | if (arrayForm) { 155 | return newja; 156 | } 157 | return newjo; 158 | } 159 | 160 | // Content, between <...> and 161 | 162 | } else { 163 | if (token != XML.GT) { 164 | throw x.syntaxError("Misshaped tag"); 165 | } 166 | closeTag = (String)parse(x, arrayForm, newja, keepStrings); 167 | if (closeTag != null) { 168 | if (!closeTag.equals(tagName)) { 169 | throw x.syntaxError("Mismatched '" + tagName + 170 | "' and '" + closeTag + "'"); 171 | } 172 | tagName = null; 173 | if (!arrayForm && newja.length() > 0) { 174 | newjo.put("childNodes", newja); 175 | } 176 | if (ja == null) { 177 | if (arrayForm) { 178 | return newja; 179 | } 180 | return newjo; 181 | } 182 | } 183 | } 184 | } 185 | } else { 186 | if (ja != null) { 187 | ja.put(token instanceof String 188 | ? keepStrings ? XML.unescape((String)token) :XML.stringToValue((String)token) 189 | : token); 190 | } 191 | } 192 | } 193 | } 194 | 195 | 196 | 197 | public static JSONArray toJSONArray(String string) throws JSONException { 198 | return (JSONArray)parse(new XMLTokener(string), true, null, false); 199 | } 200 | 201 | 202 | 203 | public static JSONArray toJSONArray(String string, boolean keepStrings) throws JSONException { 204 | return (JSONArray)parse(new XMLTokener(string), true, null, keepStrings); 205 | } 206 | 207 | 208 | 209 | public static JSONArray toJSONArray(XMLTokener x, boolean keepStrings) throws JSONException { 210 | return (JSONArray)parse(x, true, null, keepStrings); 211 | } 212 | 213 | 214 | 215 | public static JSONArray toJSONArray(XMLTokener x) throws JSONException { 216 | return (JSONArray)parse(x, true, null, false); 217 | } 218 | 219 | 220 | 221 | public static JSONObject toJSONObject(String string) throws JSONException { 222 | return (JSONObject)parse(new XMLTokener(string), false, null, false); 223 | } 224 | 225 | 226 | 227 | public static JSONObject toJSONObject(String string, boolean keepStrings) throws JSONException { 228 | return (JSONObject)parse(new XMLTokener(string), false, null, keepStrings); 229 | } 230 | 231 | 232 | 233 | public static JSONObject toJSONObject(XMLTokener x) throws JSONException { 234 | return (JSONObject)parse(x, false, null, false); 235 | } 236 | 237 | 238 | 239 | public static JSONObject toJSONObject(XMLTokener x, boolean keepStrings) throws JSONException { 240 | return (JSONObject)parse(x, false, null, keepStrings); 241 | } 242 | 243 | 244 | 245 | public static String toString(JSONArray ja) throws JSONException { 246 | int i; 247 | JSONObject jo; 248 | int length; 249 | Object object; 250 | StringBuilder sb = new StringBuilder(); 251 | String tagName; 252 | 253 | // Emit = length) { 289 | sb.append('/'); 290 | sb.append('>'); 291 | } else { 292 | sb.append('>'); 293 | do { 294 | object = ja.get(i); 295 | i += 1; 296 | if (object != null) { 297 | if (object instanceof String) { 298 | sb.append(XML.escape(object.toString())); 299 | } else if (object instanceof JSONObject) { 300 | sb.append(toString((JSONObject)object)); 301 | } else if (object instanceof JSONArray) { 302 | sb.append(toString((JSONArray)object)); 303 | } else { 304 | sb.append(object.toString()); 305 | } 306 | } 307 | } while (i < length); 308 | sb.append('<'); 309 | sb.append('/'); 310 | sb.append(tagName); 311 | sb.append('>'); 312 | } 313 | return sb.toString(); 314 | } 315 | 316 | 317 | public static String toString(JSONObject jo) throws JSONException { 318 | StringBuilder sb = new StringBuilder(); 319 | int i; 320 | JSONArray ja; 321 | int length; 322 | Object object; 323 | String tagName; 324 | Object value; 325 | 326 | //Emit '); 361 | } else { 362 | sb.append('>'); 363 | length = ja.length(); 364 | for (i = 0; i < length; i += 1) { 365 | object = ja.get(i); 366 | if (object != null) { 367 | if (object instanceof String) { 368 | sb.append(XML.escape(object.toString())); 369 | } else if (object instanceof JSONObject) { 370 | sb.append(toString((JSONObject)object)); 371 | } else if (object instanceof JSONArray) { 372 | sb.append(toString((JSONArray)object)); 373 | } else { 374 | sb.append(object.toString()); 375 | } 376 | } 377 | } 378 | sb.append('<'); 379 | sb.append('/'); 380 | sb.append(tagName); 381 | sb.append('>'); 382 | } 383 | return sb.toString(); 384 | } 385 | } 386 | -------------------------------------------------------------------------------- /src/main/java/com/browserstack/json/JSONPointer.java: -------------------------------------------------------------------------------- 1 | package com.browserstack.json; 2 | 3 | import static java.lang.String.format; 4 | 5 | import java.io.UnsupportedEncodingException; 6 | import java.net.URLDecoder; 7 | import java.net.URLEncoder; 8 | import java.util.ArrayList; 9 | import java.util.Collections; 10 | import java.util.List; 11 | 12 | 13 | 14 | 15 | public class JSONPointer { 16 | 17 | 18 | private static final String ENCODING = "utf-8"; 19 | 20 | 21 | public static class Builder { 22 | 23 | 24 | private final List refTokens = new ArrayList(); 25 | 26 | 27 | public JSONPointer build() { 28 | return new JSONPointer(this.refTokens); 29 | } 30 | 31 | 32 | public Builder append(String token) { 33 | if (token == null) { 34 | throw new NullPointerException("token cannot be null"); 35 | } 36 | this.refTokens.add(token); 37 | return this; 38 | } 39 | 40 | 41 | public Builder append(int arrayIndex) { 42 | this.refTokens.add(String.valueOf(arrayIndex)); 43 | return this; 44 | } 45 | } 46 | 47 | 48 | public static Builder builder() { 49 | return new Builder(); 50 | } 51 | 52 | 53 | private final List refTokens; 54 | 55 | 56 | public JSONPointer(final String pointer) { 57 | if (pointer == null) { 58 | throw new NullPointerException("pointer cannot be null"); 59 | } 60 | if (pointer.isEmpty() || pointer.equals("#")) { 61 | this.refTokens = Collections.emptyList(); 62 | return; 63 | } 64 | String refs; 65 | if (pointer.startsWith("#/")) { 66 | refs = pointer.substring(2); 67 | try { 68 | refs = URLDecoder.decode(refs, ENCODING); 69 | } catch (UnsupportedEncodingException e) { 70 | throw new RuntimeException(e); 71 | } 72 | } else if (pointer.startsWith("/")) { 73 | refs = pointer.substring(1); 74 | } else { 75 | throw new IllegalArgumentException("a JSON pointer should start with '/' or '#/'"); 76 | } 77 | this.refTokens = new ArrayList(); 78 | int slashIdx = -1; 79 | int prevSlashIdx = 0; 80 | do { 81 | prevSlashIdx = slashIdx + 1; 82 | slashIdx = refs.indexOf('/', prevSlashIdx); 83 | if(prevSlashIdx == slashIdx || prevSlashIdx == refs.length()) { 84 | 85 | 86 | this.refTokens.add(""); 87 | } else if (slashIdx >= 0) { 88 | final String token = refs.substring(prevSlashIdx, slashIdx); 89 | this.refTokens.add(unescape(token)); 90 | } else { 91 | 92 | final String token = refs.substring(prevSlashIdx); 93 | this.refTokens.add(unescape(token)); 94 | } 95 | } while (slashIdx >= 0); 96 | 97 | 98 | 99 | 100 | } 101 | 102 | public JSONPointer(List refTokens) { 103 | this.refTokens = new ArrayList(refTokens); 104 | } 105 | 106 | private String unescape(String token) { 107 | return token.replace("~1", "/").replace("~0", "~") 108 | .replace("\\\"", "\"") 109 | .replace("\\\\", "\\"); 110 | } 111 | 112 | 113 | public Object queryFrom(Object document) throws JSONPointerException { 114 | if (this.refTokens.isEmpty()) { 115 | return document; 116 | } 117 | Object current = document; 118 | for (String token : this.refTokens) { 119 | if (current instanceof JSONObject) { 120 | current = ((JSONObject) current).opt(unescape(token)); 121 | } else if (current instanceof JSONArray) { 122 | current = readByIndexToken(current, token); 123 | } else { 124 | throw new JSONPointerException(format( 125 | "value [%s] is not an array or object therefore its key %s cannot be resolved", current, 126 | token)); 127 | } 128 | } 129 | return current; 130 | } 131 | 132 | 133 | private Object readByIndexToken(Object current, String indexToken) throws JSONPointerException { 134 | try { 135 | int index = Integer.parseInt(indexToken); 136 | JSONArray currentArr = (JSONArray) current; 137 | if (index >= currentArr.length()) { 138 | throw new JSONPointerException(format("index %d is out of bounds - the array has %d elements", index, 139 | currentArr.length())); 140 | } 141 | try { 142 | return currentArr.get(index); 143 | } catch (JSONException e) { 144 | throw new JSONPointerException("Error reading value at index position " + index, e); 145 | } 146 | } catch (NumberFormatException e) { 147 | throw new JSONPointerException(format("%s is not an array index", indexToken), e); 148 | } 149 | } 150 | 151 | 152 | @Override 153 | public String toString() { 154 | StringBuilder rval = new StringBuilder(""); 155 | for (String token: this.refTokens) { 156 | rval.append('/').append(escape(token)); 157 | } 158 | return rval.toString(); 159 | } 160 | 161 | 162 | private String escape(String token) { 163 | return token.replace("~", "~0") 164 | .replace("/", "~1") 165 | .replace("\\", "\\\\") 166 | .replace("\"", "\\\""); 167 | } 168 | 169 | 170 | public String toURIFragment() { 171 | try { 172 | StringBuilder rval = new StringBuilder("#"); 173 | for (String token : this.refTokens) { 174 | rval.append('/').append(URLEncoder.encode(token, ENCODING)); 175 | } 176 | return rval.toString(); 177 | } catch (UnsupportedEncodingException e) { 178 | throw new RuntimeException(e); 179 | } 180 | } 181 | 182 | } 183 | -------------------------------------------------------------------------------- /src/main/java/com/browserstack/json/JSONPointerException.java: -------------------------------------------------------------------------------- 1 | package com.browserstack.json; 2 | 3 | 4 | 5 | 6 | public class JSONPointerException extends JSONException { 7 | private static final long serialVersionUID = 8872944667561856751L; 8 | 9 | public JSONPointerException(String message) { 10 | super(message); 11 | } 12 | 13 | public JSONPointerException(String message, Throwable cause) { 14 | super(message, cause); 15 | } 16 | 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/com/browserstack/json/JSONPropertyIgnore.java: -------------------------------------------------------------------------------- 1 | package com.browserstack.json; 2 | 3 | 4 | 5 | import static java.lang.annotation.ElementType.METHOD; 6 | import static java.lang.annotation.RetentionPolicy.RUNTIME; 7 | 8 | import java.lang.annotation.Documented; 9 | import java.lang.annotation.Retention; 10 | import java.lang.annotation.Target; 11 | 12 | @Documented 13 | @Retention(RUNTIME) 14 | @Target({METHOD}) 15 | 16 | public @interface JSONPropertyIgnore { } 17 | -------------------------------------------------------------------------------- /src/main/java/com/browserstack/json/JSONPropertyName.java: -------------------------------------------------------------------------------- 1 | package com.browserstack.json; 2 | 3 | 4 | 5 | import static java.lang.annotation.ElementType.METHOD; 6 | import static java.lang.annotation.RetentionPolicy.RUNTIME; 7 | 8 | import java.lang.annotation.Documented; 9 | import java.lang.annotation.Retention; 10 | import java.lang.annotation.Target; 11 | 12 | @Documented 13 | @Retention(RUNTIME) 14 | @Target({METHOD}) 15 | 16 | public @interface JSONPropertyName { 17 | 18 | String value(); 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/com/browserstack/json/JSONString.java: -------------------------------------------------------------------------------- 1 | package com.browserstack.json; 2 | 3 | public interface JSONString { 4 | 5 | public String toJSONString(); 6 | } 7 | -------------------------------------------------------------------------------- /src/main/java/com/browserstack/json/JSONStringer.java: -------------------------------------------------------------------------------- 1 | package com.browserstack.json; 2 | 3 | 4 | 5 | import java.io.StringWriter; 6 | 7 | 8 | public class JSONStringer extends JSONWriter { 9 | 10 | public JSONStringer() { 11 | super(new StringWriter()); 12 | } 13 | 14 | 15 | @Override 16 | public String toString() { 17 | return this.mode == 'd' ? this.writer.toString() : null; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/com/browserstack/json/JSONTokener.java: -------------------------------------------------------------------------------- 1 | package com.browserstack.json; 2 | 3 | import java.io.BufferedReader; 4 | import java.io.IOException; 5 | import java.io.InputStream; 6 | import java.io.InputStreamReader; 7 | import java.io.Reader; 8 | import java.io.StringReader; 9 | 10 | 11 | 12 | 13 | public class JSONTokener { 14 | 15 | private long character; 16 | 17 | private boolean eof; 18 | 19 | private long index; 20 | 21 | private long line; 22 | 23 | private char previous; 24 | 25 | private final Reader reader; 26 | 27 | private boolean usePrevious; 28 | 29 | private long characterPreviousLine; 30 | 31 | 32 | 33 | public JSONTokener(Reader reader) { 34 | this.reader = reader.markSupported() 35 | ? reader 36 | : new BufferedReader(reader); 37 | this.eof = false; 38 | this.usePrevious = false; 39 | this.previous = 0; 40 | this.index = 0; 41 | this.character = 1; 42 | this.characterPreviousLine = 0; 43 | this.line = 1; 44 | } 45 | 46 | 47 | 48 | public JSONTokener(InputStream inputStream) { 49 | this(new InputStreamReader(inputStream)); 50 | } 51 | 52 | 53 | 54 | public JSONTokener(String s) { 55 | this(new StringReader(s)); 56 | } 57 | 58 | 59 | 60 | public void back() throws JSONException { 61 | if (this.usePrevious || this.index <= 0) { 62 | throw new JSONException("Stepping back two steps is not supported"); 63 | } 64 | this.decrementIndexes(); 65 | this.usePrevious = true; 66 | this.eof = false; 67 | } 68 | 69 | 70 | private void decrementIndexes() { 71 | this.index--; 72 | if(this.previous=='\r' || this.previous == '\n') { 73 | this.line--; 74 | this.character=this.characterPreviousLine ; 75 | } else if(this.character > 0){ 76 | this.character--; 77 | } 78 | } 79 | 80 | 81 | public static int dehexchar(char c) { 82 | if (c >= '0' && c <= '9') { 83 | return c - '0'; 84 | } 85 | if (c >= 'A' && c <= 'F') { 86 | return c - ('A' - 10); 87 | } 88 | if (c >= 'a' && c <= 'f') { 89 | return c - ('a' - 10); 90 | } 91 | return -1; 92 | } 93 | 94 | 95 | public boolean end() { 96 | return this.eof && !this.usePrevious; 97 | } 98 | 99 | 100 | 101 | public boolean more() throws JSONException { 102 | if(this.usePrevious) { 103 | return true; 104 | } 105 | try { 106 | this.reader.mark(1); 107 | } catch (IOException e) { 108 | throw new JSONException("Unable to preserve stream position", e); 109 | } 110 | try { 111 | 112 | if(this.reader.read() <= 0) { 113 | this.eof = true; 114 | return false; 115 | } 116 | this.reader.reset(); 117 | } catch (IOException e) { 118 | throw new JSONException("Unable to read the next character from the stream", e); 119 | } 120 | return true; 121 | } 122 | 123 | 124 | 125 | public char next() throws JSONException { 126 | int c; 127 | if (this.usePrevious) { 128 | this.usePrevious = false; 129 | c = this.previous; 130 | } else { 131 | try { 132 | c = this.reader.read(); 133 | } catch (IOException exception) { 134 | throw new JSONException(exception); 135 | } 136 | } 137 | if (c <= 0) { 138 | this.eof = true; 139 | return 0; 140 | } 141 | this.incrementIndexes(c); 142 | this.previous = (char) c; 143 | return this.previous; 144 | } 145 | 146 | 147 | private void incrementIndexes(int c) { 148 | if(c > 0) { 149 | this.index++; 150 | if(c=='\r') { 151 | this.line++; 152 | this.characterPreviousLine = this.character; 153 | this.character=0; 154 | }else if (c=='\n') { 155 | if(this.previous != '\r') { 156 | this.line++; 157 | this.characterPreviousLine = this.character; 158 | } 159 | this.character=0; 160 | } else { 161 | this.character++; 162 | } 163 | } 164 | } 165 | 166 | 167 | public char next(char c) throws JSONException { 168 | char n = this.next(); 169 | if (n != c) { 170 | if(n > 0) { 171 | throw this.syntaxError("Expected '" + c + "' and instead saw '" + 172 | n + "'"); 173 | } 174 | throw this.syntaxError("Expected '" + c + "' and instead saw ''"); 175 | } 176 | return n; 177 | } 178 | 179 | 180 | 181 | public String next(int n) throws JSONException { 182 | if (n == 0) { 183 | return ""; 184 | } 185 | 186 | char[] chars = new char[n]; 187 | int pos = 0; 188 | 189 | while (pos < n) { 190 | chars[pos] = this.next(); 191 | if (this.end()) { 192 | throw this.syntaxError("Substring bounds error"); 193 | } 194 | pos += 1; 195 | } 196 | return new String(chars); 197 | } 198 | 199 | 200 | 201 | public char nextClean() throws JSONException { 202 | for (;;) { 203 | char c = this.next(); 204 | if (c == 0 || c > ' ') { 205 | return c; 206 | } 207 | } 208 | } 209 | 210 | 211 | 212 | public String nextString(char quote) throws JSONException { 213 | char c; 214 | StringBuilder sb = new StringBuilder(); 215 | for (;;) { 216 | c = this.next(); 217 | switch (c) { 218 | case 0: 219 | case '\n': 220 | case '\r': 221 | throw this.syntaxError("Unterminated string"); 222 | case '\\': 223 | c = this.next(); 224 | switch (c) { 225 | case 'b': 226 | sb.append('\b'); 227 | break; 228 | case 't': 229 | sb.append('\t'); 230 | break; 231 | case 'n': 232 | sb.append('\n'); 233 | break; 234 | case 'f': 235 | sb.append('\f'); 236 | break; 237 | case 'r': 238 | sb.append('\r'); 239 | break; 240 | case 'u': 241 | try { 242 | sb.append((char)Integer.parseInt(this.next(4), 16)); 243 | } catch (NumberFormatException e) { 244 | throw this.syntaxError("Illegal escape.", e); 245 | } 246 | break; 247 | case '"': 248 | case '\'': 249 | case '\\': 250 | case '/': 251 | sb.append(c); 252 | break; 253 | default: 254 | throw this.syntaxError("Illegal escape."); 255 | } 256 | break; 257 | default: 258 | if (c == quote) { 259 | return sb.toString(); 260 | } 261 | sb.append(c); 262 | } 263 | } 264 | } 265 | 266 | 267 | 268 | public String nextTo(char delimiter) throws JSONException { 269 | StringBuilder sb = new StringBuilder(); 270 | for (;;) { 271 | char c = this.next(); 272 | if (c == delimiter || c == 0 || c == '\n' || c == '\r') { 273 | if (c != 0) { 274 | this.back(); 275 | } 276 | return sb.toString().trim(); 277 | } 278 | sb.append(c); 279 | } 280 | } 281 | 282 | 283 | 284 | public String nextTo(String delimiters) throws JSONException { 285 | char c; 286 | StringBuilder sb = new StringBuilder(); 287 | for (;;) { 288 | c = this.next(); 289 | if (delimiters.indexOf(c) >= 0 || c == 0 || 290 | c == '\n' || c == '\r') { 291 | if (c != 0) { 292 | this.back(); 293 | } 294 | return sb.toString().trim(); 295 | } 296 | sb.append(c); 297 | } 298 | } 299 | 300 | 301 | 302 | public Object nextValue() throws JSONException { 303 | char c = this.nextClean(); 304 | String string; 305 | 306 | switch (c) { 307 | case '"': 308 | case '\'': 309 | return this.nextString(c); 310 | case '{': 311 | this.back(); 312 | return new JSONObject(this); 313 | case '[': 314 | this.back(); 315 | return new JSONArray(this); 316 | } 317 | 318 | 319 | 320 | StringBuilder sb = new StringBuilder(); 321 | while (c >= ' ' && ",:]}/\\\"[{;=#".indexOf(c) < 0) { 322 | sb.append(c); 323 | c = this.next(); 324 | } 325 | this.back(); 326 | 327 | string = sb.toString().trim(); 328 | if ("".equals(string)) { 329 | throw this.syntaxError("Missing value"); 330 | } 331 | return JSONObject.stringToValue(string); 332 | } 333 | 334 | 335 | 336 | public char skipTo(char to) throws JSONException { 337 | char c; 338 | try { 339 | long startIndex = this.index; 340 | long startCharacter = this.character; 341 | long startLine = this.line; 342 | this.reader.mark(1000000); 343 | do { 344 | c = this.next(); 345 | if (c == 0) { 346 | 347 | 348 | 349 | this.reader.reset(); 350 | this.index = startIndex; 351 | this.character = startCharacter; 352 | this.line = startLine; 353 | return 0; 354 | } 355 | } while (c != to); 356 | this.reader.mark(1); 357 | } catch (IOException exception) { 358 | throw new JSONException(exception); 359 | } 360 | this.back(); 361 | return c; 362 | } 363 | 364 | 365 | public JSONException syntaxError(String message) { 366 | return new JSONException(message + this.toString()); 367 | } 368 | 369 | 370 | public JSONException syntaxError(String message, Throwable causedBy) { 371 | return new JSONException(message + this.toString(), causedBy); 372 | } 373 | 374 | 375 | @Override 376 | public String toString() { 377 | return " at " + this.index + " [character " + this.character + " line " + 378 | this.line + "]"; 379 | } 380 | } 381 | -------------------------------------------------------------------------------- /src/main/java/com/browserstack/json/JSONWriter.java: -------------------------------------------------------------------------------- 1 | package com.browserstack.json; 2 | 3 | import java.io.IOException; 4 | import java.math.BigDecimal; 5 | import java.util.Collection; 6 | import java.util.Map; 7 | 8 | 9 | 10 | 11 | public class JSONWriter { 12 | private static final int maxdepth = 200; 13 | 14 | 15 | private boolean comma; 16 | 17 | 18 | protected char mode; 19 | 20 | 21 | private final JSONObject stack[]; 22 | 23 | 24 | private int top; 25 | 26 | 27 | protected Appendable writer; 28 | 29 | 30 | public JSONWriter(Appendable w) { 31 | this.comma = false; 32 | this.mode = 'i'; 33 | this.stack = new JSONObject[maxdepth]; 34 | this.top = 0; 35 | this.writer = w; 36 | } 37 | 38 | 39 | private JSONWriter append(String string) throws JSONException { 40 | if (string == null) { 41 | throw new JSONException("Null pointer"); 42 | } 43 | if (this.mode == 'o' || this.mode == 'a') { 44 | try { 45 | if (this.comma && this.mode == 'a') { 46 | this.writer.append(','); 47 | } 48 | this.writer.append(string); 49 | } catch (IOException e) { 50 | // Android as of API 25 does not support this exception constructor 51 | // however we won't worry about it. If an exception is happening here 52 | // it will just throw a "Method not found" exception instead. 53 | throw new JSONException(e); 54 | } 55 | if (this.mode == 'o') { 56 | this.mode = 'k'; 57 | } 58 | this.comma = true; 59 | return this; 60 | } 61 | throw new JSONException("Value out of sequence."); 62 | } 63 | 64 | 65 | public JSONWriter array() throws JSONException { 66 | if (this.mode == 'i' || this.mode == 'o' || this.mode == 'a') { 67 | this.push(null); 68 | this.append("["); 69 | this.comma = false; 70 | return this; 71 | } 72 | throw new JSONException("Misplaced array."); 73 | } 74 | 75 | 76 | private JSONWriter end(char m, char c) throws JSONException { 77 | if (this.mode != m) { 78 | throw new JSONException(m == 'a' 79 | ? "Misplaced endArray." 80 | : "Misplaced endObject."); 81 | } 82 | this.pop(m); 83 | try { 84 | this.writer.append(c); 85 | } catch (IOException e) { 86 | // Android as of API 25 does not support this exception constructor 87 | // however we won't worry about it. If an exception is happening here 88 | // it will just throw a "Method not found" exception instead. 89 | throw new JSONException(e); 90 | } 91 | this.comma = true; 92 | return this; 93 | } 94 | 95 | 96 | public JSONWriter endArray() throws JSONException { 97 | return this.end('a', ']'); 98 | } 99 | 100 | 101 | public JSONWriter endObject() throws JSONException { 102 | return this.end('k', '}'); 103 | } 104 | 105 | 106 | public JSONWriter key(String string) throws JSONException { 107 | if (string == null) { 108 | throw new JSONException("Null key."); 109 | } 110 | if (this.mode == 'k') { 111 | try { 112 | JSONObject topObject = this.stack[this.top - 1]; 113 | 114 | if(topObject.has(string)) { 115 | throw new JSONException("Duplicate key \"" + string + "\""); 116 | } 117 | topObject.put(string, true); 118 | if (this.comma) { 119 | this.writer.append(','); 120 | } 121 | this.writer.append(JSONObject.quote(string)); 122 | this.writer.append(':'); 123 | this.comma = false; 124 | this.mode = 'o'; 125 | return this; 126 | } catch (IOException e) { 127 | // Android as of API 25 does not support this exception constructor 128 | // however we won't worry about it. If an exception is happening here 129 | // it will just throw a "Method not found" exception instead. 130 | throw new JSONException(e); 131 | } 132 | } 133 | throw new JSONException("Misplaced key."); 134 | } 135 | 136 | 137 | 138 | public JSONWriter object() throws JSONException { 139 | if (this.mode == 'i') { 140 | this.mode = 'o'; 141 | } 142 | if (this.mode == 'o' || this.mode == 'a') { 143 | this.append("{"); 144 | this.push(new JSONObject()); 145 | this.comma = false; 146 | return this; 147 | } 148 | throw new JSONException("Misplaced object."); 149 | 150 | } 151 | 152 | 153 | 154 | private void pop(char c) throws JSONException { 155 | if (this.top <= 0) { 156 | throw new JSONException("Nesting error."); 157 | } 158 | char m = this.stack[this.top - 1] == null ? 'a' : 'k'; 159 | if (m != c) { 160 | throw new JSONException("Nesting error."); 161 | } 162 | this.top -= 1; 163 | this.mode = this.top == 0 164 | ? 'd' 165 | : this.stack[this.top - 1] == null 166 | ? 'a' 167 | : 'k'; 168 | } 169 | 170 | 171 | private void push(JSONObject jo) throws JSONException { 172 | if (this.top >= maxdepth) { 173 | throw new JSONException("Nesting too deep."); 174 | } 175 | this.stack[this.top] = jo; 176 | this.mode = jo == null ? 'a' : 'k'; 177 | this.top += 1; 178 | } 179 | 180 | 181 | public static String valueToString(Object value) throws JSONException { 182 | if (value == null || value.equals(null)) { 183 | return "null"; 184 | } 185 | if (value instanceof JSONString) { 186 | Object object; 187 | try { 188 | object = ((JSONString) value).toJSONString(); 189 | } catch (Exception e) { 190 | throw new JSONException(e); 191 | } 192 | if (object instanceof String) { 193 | return (String) object; 194 | } 195 | throw new JSONException("Bad value from toJSONString: " + object); 196 | } 197 | if (value instanceof Number) { 198 | 199 | final String numberAsString = JSONObject.numberToString((Number) value); 200 | try { 201 | 202 | @SuppressWarnings("unused") 203 | BigDecimal unused = new BigDecimal(numberAsString); 204 | 205 | return numberAsString; 206 | } catch (NumberFormatException ex){ 207 | 208 | 209 | return JSONObject.quote(numberAsString); 210 | } 211 | } 212 | if (value instanceof Boolean || value instanceof JSONObject 213 | || value instanceof JSONArray) { 214 | return value.toString(); 215 | } 216 | if (value instanceof Map) { 217 | Map map = (Map) value; 218 | return new JSONObject(map).toString(); 219 | } 220 | if (value instanceof Collection) { 221 | Collection coll = (Collection) value; 222 | return new JSONArray(coll).toString(); 223 | } 224 | if (value.getClass().isArray()) { 225 | return new JSONArray(value).toString(); 226 | } 227 | if(value instanceof Enum){ 228 | return JSONObject.quote(((Enum)value).name()); 229 | } 230 | return JSONObject.quote(value.toString()); 231 | } 232 | 233 | 234 | public JSONWriter value(boolean b) throws JSONException { 235 | return this.append(b ? "true" : "false"); 236 | } 237 | 238 | 239 | public JSONWriter value(double d) throws JSONException { 240 | return this.value(new Double(d)); 241 | } 242 | 243 | 244 | public JSONWriter value(long l) throws JSONException { 245 | return this.append(Long.toString(l)); 246 | } 247 | 248 | 249 | 250 | public JSONWriter value(Object object) throws JSONException { 251 | return this.append(valueToString(object)); 252 | } 253 | } 254 | -------------------------------------------------------------------------------- /src/main/java/com/browserstack/json/LICENSE: -------------------------------------------------------------------------------- 1 | ============================================================================ 2 | 3 | Copyright (c) 2002 JSON.org 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | The Software shall be used for Good, not Evil. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | SOFTWARE. 24 | -------------------------------------------------------------------------------- /src/main/java/com/browserstack/json/Property.java: -------------------------------------------------------------------------------- 1 | package com.browserstack.json; 2 | 3 | 4 | 5 | import java.util.Enumeration; 6 | import java.util.Properties; 7 | 8 | 9 | public class Property { 10 | 11 | public static JSONObject toJSONObject(java.util.Properties properties) throws JSONException { 12 | 13 | 14 | JSONObject jo = new JSONObject(); 15 | if (properties != null && !properties.isEmpty()) { 16 | Enumeration enumProperties = properties.propertyNames(); 17 | while(enumProperties.hasMoreElements()) { 18 | String name = (String)enumProperties.nextElement(); 19 | jo.put(name, properties.getProperty(name)); 20 | } 21 | } 22 | return jo; 23 | } 24 | 25 | 26 | public static Properties toProperties(JSONObject jo) throws JSONException { 27 | Properties properties = new Properties(); 28 | if (jo != null) { 29 | // Don't use the new entrySet API to maintain Android support 30 | for (final String key : jo.keySet()) { 31 | Object value = jo.opt(key); 32 | if (!JSONObject.NULL.equals(value)) { 33 | properties.put(key, value.toString()); 34 | } 35 | } 36 | } 37 | return properties; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/main/java/com/browserstack/json/README.md: -------------------------------------------------------------------------------- 1 | JSON in Java [package org.json] 2 | =============================== 3 | 4 | [![Maven Central](https://img.shields.io/maven-central/v/org.json/json.svg)](https://mvnrepository.com/artifact/org.json/json) 5 | 6 | JSON is a light-weight, language independent, data interchange format. 7 | See http://www.JSON.org/ 8 | 9 | The files in this package implement JSON encoders/decoders in Java. 10 | It also includes the capability to convert between JSON and XML, HTTP 11 | headers, Cookies, and CDL. 12 | 13 | This is a reference implementation. There is a large number of JSON packages 14 | in Java. Perhaps someday the Java community will standardize on one. Until 15 | then, choose carefully. 16 | 17 | The license includes this restriction: "The software shall be used for good, 18 | not evil." If your conscience cannot live with that, then choose a different 19 | package. 20 | 21 | The package compiles on Java 1.6-1.8. 22 | 23 | 24 | **JSONObject.java**: The `JSONObject` can parse text from a `String` or a `JSONTokener` 25 | to produce a map-like object. The object provides methods for manipulating its 26 | contents, and for producing a JSON compliant object serialization. 27 | 28 | **JSONArray.java**: The `JSONArray` can parse text from a String or a `JSONTokener` 29 | to produce a vector-like object. The object provides methods for manipulating 30 | its contents, and for producing a JSON compliant array serialization. 31 | 32 | **JSONTokener.java**: The `JSONTokener` breaks a text into a sequence of individual 33 | tokens. It can be constructed from a `String`, `Reader`, or `InputStream`. 34 | 35 | **JSONException.java**: The `JSONException` is the standard exception type thrown 36 | by this package. 37 | 38 | **JSONPointer.java**: Implementation of 39 | [JSON Pointer (RFC 6901)](https://tools.ietf.org/html/rfc6901). Supports 40 | JSON Pointers both in the form of string representation and URI fragment 41 | representation. 42 | 43 | **JSONPropertyIgnore.java**: Annotation class that can be used on Java Bean getter methods. 44 | When used on a bean method that would normally be serialized into a `JSONObject`, it 45 | overrides the getter-to-key-name logic and forces the property to be excluded from the 46 | resulting `JSONObject`. 47 | 48 | **JSONPropertyName.java**: Annotation class that can be used on Java Bean getter methods. 49 | When used on a bean method that would normally be serialized into a `JSONObject`, it 50 | overrides the getter-to-key-name logic and uses the value of the annotation. The Bean 51 | processor will look through the class hierarchy. This means you can use the annotation on 52 | a base class or interface and the value of the annotation will be used even if the getter 53 | is overridden in a child class. 54 | 55 | **JSONString.java**: The `JSONString` interface requires a `toJSONString` method, 56 | allowing an object to provide its own serialization. 57 | 58 | **JSONStringer.java**: The `JSONStringer` provides a convenient facility for 59 | building JSON strings. 60 | 61 | **JSONWriter.java**: The `JSONWriter` provides a convenient facility for building 62 | JSON text through a writer. 63 | 64 | 65 | **CDL.java**: `CDL` provides support for converting between JSON and comma 66 | delimited lists. 67 | 68 | **Cookie.java**: `Cookie` provides support for converting between JSON and cookies. 69 | 70 | **CookieList.java**: `CookieList` provides support for converting between JSON and 71 | cookie lists. 72 | 73 | **HTTP.java**: `HTTP` provides support for converting between JSON and HTTP headers. 74 | 75 | **HTTPTokener.java**: `HTTPTokener` extends `JSONTokener` for parsing HTTP headers. 76 | 77 | **XML.java**: `XML` provides support for converting between JSON and XML. 78 | 79 | **JSONML.java**: `JSONML` provides support for converting between JSONML and XML. 80 | 81 | **XMLTokener.java**: `XMLTokener` extends `JSONTokener` for parsing XML text. 82 | 83 | Unit tests are maintained in a separate project. Contributing developers can test 84 | JSON-java pull requests with the code in this project: 85 | https://github.com/stleary/JSON-Java-unit-test 86 | 87 | Numeric types in this package comply with 88 | [ECMA-404: The JSON Data Interchange Format](http://www.ecma-international.org/publications/files/ECMA-ST/ECMA-404.pdf) and 89 | [RFC 7159: The JavaScript Object Notation (JSON) Data Interchange Format](https://tools.ietf.org/html/rfc7159#section-6). 90 | This package fully supports `Integer`, `Long`, and `Double` Java types. Partial support 91 | for `BigInteger` and `BigDecimal` values in `JSONObject` and `JSONArray` objects is provided 92 | in the form of `get()`, `opt()`, and `put()` API methods. 93 | 94 | Although 1.6 compatibility is currently supported, it is not a project goal and may be 95 | removed in some future release. 96 | 97 | In compliance with RFC7159 page 10 section 9, the parser is more lax with what is valid 98 | JSON than the Generator. For Example, the tab character (U+0009) is allowed when reading 99 | JSON Text strings, but when output by the Generator, tab is properly converted to \t in 100 | the string. Other instances may occur where reading invalid JSON text does not cause an 101 | error to be generated. Malformed JSON Texts such as missing end " (quote) on strings or 102 | invalid number formats (1.2e6.3) will cause errors as such documents can not be read 103 | reliably. 104 | 105 | Release history: 106 | 107 | ~~~ 108 | 20180130 Recent commits 109 | 110 | 20171018 Checkpoint for recent commits. 111 | 112 | 20170516 Roll up recent commits. 113 | 114 | 20160810 Revert code that was breaking opt*() methods. 115 | 116 | 20160807 This release contains a bug in the JSONObject.opt*() and JSONArray.opt*() methods, 117 | it is not recommended for use. 118 | Java 1.6 compatability fixed, JSONArray.toList() and JSONObject.toMap(), 119 | RFC4180 compatibility, JSONPointer, some exception fixes, optional XML type conversion. 120 | Contains the latest code as of 7 Aug, 2016 121 | 122 | 20160212 Java 1.6 compatibility, OSGi bundle. Contains the latest code as of 12 Feb, 2016. 123 | 124 | 20151123 JSONObject and JSONArray initialization with generics. Contains the 125 | latest code as of 23 Nov, 2015. 126 | 127 | 20150729 Checkpoint for Maven central repository release. Contains the latest code 128 | as of 29 July, 2015. 129 | ~~~ 130 | 131 | 132 | JSON-java releases can be found by searching the Maven repository for groupId "org.json" 133 | and artifactId "json". For example: 134 | https://search.maven.org/#search%7Cgav%7C1%7Cg%3A%22org.json%22%20AND%20a%3A%22json%22 135 | -------------------------------------------------------------------------------- /src/main/java/com/browserstack/json/XML.java: -------------------------------------------------------------------------------- 1 | package com.browserstack.json; 2 | 3 | 4 | 5 | import java.io.Reader; 6 | import java.io.StringReader; 7 | import java.util.Iterator; 8 | 9 | 10 | @SuppressWarnings("boxing") 11 | public class XML { 12 | 13 | public static final Character AMP = '&'; 14 | 15 | 16 | public static final Character APOS = '\''; 17 | 18 | 19 | public static final Character BANG = '!'; 20 | 21 | 22 | public static final Character EQ = '='; 23 | 24 | 25 | public static final Character GT = '>'; 26 | 27 | 28 | public static final Character LT = '<'; 29 | 30 | 31 | public static final Character QUEST = '?'; 32 | 33 | 34 | public static final Character QUOT = '"'; 35 | 36 | 37 | public static final Character SLASH = '/'; 38 | 39 | 40 | private static Iterable codePointIterator(final String string) { 41 | return new Iterable() { 42 | @Override 43 | public Iterator iterator() { 44 | return new Iterator() { 45 | private int nextIndex = 0; 46 | private int length = string.length(); 47 | 48 | @Override 49 | public boolean hasNext() { 50 | return this.nextIndex < this.length; 51 | } 52 | 53 | @Override 54 | public Integer next() { 55 | int result = string.codePointAt(this.nextIndex); 56 | this.nextIndex += Character.charCount(result); 57 | return result; 58 | } 59 | 60 | @Override 61 | public void remove() { 62 | throw new UnsupportedOperationException(); 63 | } 64 | }; 65 | } 66 | }; 67 | } 68 | 69 | 70 | public static String escape(String string) { 71 | StringBuilder sb = new StringBuilder(string.length()); 72 | for (final int cp : codePointIterator(string)) { 73 | switch (cp) { 74 | case '&': 75 | sb.append("&"); 76 | break; 77 | case '<': 78 | sb.append("<"); 79 | break; 80 | case '>': 81 | sb.append(">"); 82 | break; 83 | case '"': 84 | sb.append("""); 85 | break; 86 | case '\'': 87 | sb.append("'"); 88 | break; 89 | default: 90 | if (mustEscape(cp)) { 91 | sb.append("&#x"); 92 | sb.append(Integer.toHexString(cp)); 93 | sb.append(';'); 94 | } else { 95 | sb.appendCodePoint(cp); 96 | } 97 | } 98 | } 99 | return sb.toString(); 100 | } 101 | 102 | 103 | private static boolean mustEscape(int cp) { 104 | 105 | 106 | 107 | return (Character.isISOControl(cp) 108 | && cp != 0x9 109 | && cp != 0xA 110 | && cp != 0xD 111 | ) || !( 112 | 113 | (cp >= 0x20 && cp <= 0xD7FF) 114 | || (cp >= 0xE000 && cp <= 0xFFFD) 115 | || (cp >= 0x10000 && cp <= 0x10FFFF) 116 | ) 117 | ; 118 | } 119 | 120 | 121 | public static String unescape(String string) { 122 | StringBuilder sb = new StringBuilder(string.length()); 123 | for (int i = 0, length = string.length(); i < length; i++) { 124 | char c = string.charAt(i); 125 | if (c == '&') { 126 | final int semic = string.indexOf(';', i); 127 | if (semic > i) { 128 | final String entity = string.substring(i + 1, semic); 129 | sb.append(XMLTokener.unescapeEntity(entity)); 130 | 131 | i += entity.length() + 1; 132 | } else { 133 | 134 | 135 | sb.append(c); 136 | } 137 | } else { 138 | 139 | sb.append(c); 140 | } 141 | } 142 | return sb.toString(); 143 | } 144 | 145 | 146 | public static void noSpace(String string) throws JSONException { 147 | int i, length = string.length(); 148 | if (length == 0) { 149 | throw new JSONException("Empty string."); 150 | } 151 | for (i = 0; i < length; i += 1) { 152 | if (Character.isWhitespace(string.charAt(i))) { 153 | throw new JSONException("'" + string 154 | + "' contains a space character."); 155 | } 156 | } 157 | } 158 | 159 | 160 | private static boolean parse(XMLTokener x, JSONObject context, String name, boolean keepStrings) 161 | throws JSONException { 162 | char c; 163 | int i; 164 | JSONObject jsonobject = null; 165 | String string; 166 | String tagName; 167 | Object token; 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | token = x.nextToken(); 180 | 181 | 182 | 183 | if (token == BANG) { 184 | c = x.next(); 185 | if (c == '-') { 186 | if (x.next() == '-') { 187 | x.skipPast("-->"); 188 | return false; 189 | } 190 | x.back(); 191 | } else if (c == '[') { 192 | token = x.nextToken(); 193 | if ("CDATA".equals(token)) { 194 | if (x.next() == '[') { 195 | string = x.nextCDATA(); 196 | if (string.length() > 0) { 197 | context.accumulate("content", string); 198 | } 199 | return false; 200 | } 201 | } 202 | throw x.syntaxError("Expected 'CDATA['"); 203 | } 204 | i = 1; 205 | do { 206 | token = x.nextMeta(); 207 | if (token == null) { 208 | throw x.syntaxError("Missing '>' after ' 0); 215 | return false; 216 | } else if (token == QUEST) { 217 | 218 | 219 | x.skipPast("?>"); 220 | return false; 221 | } else if (token == SLASH) { 222 | 223 | 224 | 225 | token = x.nextToken(); 226 | if (name == null) { 227 | throw x.syntaxError("Mismatched close tag " + token); 228 | } 229 | if (!token.equals(name)) { 230 | throw x.syntaxError("Mismatched " + name + " and " + token); 231 | } 232 | if (x.nextToken() != GT) { 233 | throw x.syntaxError("Misshaped close tag"); 234 | } 235 | return true; 236 | 237 | } else if (token instanceof Character) { 238 | throw x.syntaxError("Misshaped tag"); 239 | 240 | 241 | 242 | } else { 243 | tagName = (String) token; 244 | token = null; 245 | jsonobject = new JSONObject(); 246 | for (;;) { 247 | if (token == null) { 248 | token = x.nextToken(); 249 | } 250 | 251 | if (token instanceof String) { 252 | string = (String) token; 253 | token = x.nextToken(); 254 | if (token == EQ) { 255 | token = x.nextToken(); 256 | if (!(token instanceof String)) { 257 | throw x.syntaxError("Missing value"); 258 | } 259 | jsonobject.accumulate(string, 260 | keepStrings ? ((String)token) : stringToValue((String) token)); 261 | token = null; 262 | } else { 263 | jsonobject.accumulate(string, ""); 264 | } 265 | 266 | 267 | } else if (token == SLASH) { 268 | 269 | if (x.nextToken() != GT) { 270 | throw x.syntaxError("Misshaped tag"); 271 | } 272 | if (jsonobject.length() > 0) { 273 | context.accumulate(tagName, jsonobject); 274 | } else { 275 | context.accumulate(tagName, ""); 276 | } 277 | return false; 278 | 279 | } else if (token == GT) { 280 | 281 | for (;;) { 282 | token = x.nextContent(); 283 | if (token == null) { 284 | if (tagName != null) { 285 | throw x.syntaxError("Unclosed tag " + tagName); 286 | } 287 | return false; 288 | } else if (token instanceof String) { 289 | string = (String) token; 290 | if (string.length() > 0) { 291 | jsonobject.accumulate("content", 292 | keepStrings ? string : stringToValue(string)); 293 | } 294 | 295 | } else if (token == LT) { 296 | 297 | if (parse(x, jsonobject, tagName,keepStrings)) { 298 | if (jsonobject.length() == 0) { 299 | context.accumulate(tagName, ""); 300 | } else if (jsonobject.length() == 1 301 | && jsonobject.opt("content") != null) { 302 | context.accumulate(tagName, 303 | jsonobject.opt("content")); 304 | } else { 305 | context.accumulate(tagName, jsonobject); 306 | } 307 | return false; 308 | } 309 | } 310 | } 311 | } else { 312 | throw x.syntaxError("Misshaped tag"); 313 | } 314 | } 315 | } 316 | } 317 | 318 | 319 | 320 | 321 | public static Object stringToValue(String string) { 322 | if (string.equals("")) { 323 | return string; 324 | } 325 | if (string.equalsIgnoreCase("true")) { 326 | return Boolean.TRUE; 327 | } 328 | if (string.equalsIgnoreCase("false")) { 329 | return Boolean.FALSE; 330 | } 331 | if (string.equalsIgnoreCase("null")) { 332 | return JSONObject.NULL; 333 | } 334 | 335 | 336 | 337 | char initial = string.charAt(0); 338 | if ((initial >= '0' && initial <= '9') || initial == '-') { 339 | try { 340 | 341 | 342 | if (string.indexOf('.') > -1 || string.indexOf('e') > -1 343 | || string.indexOf('E') > -1 || "-0".equals(string)) { 344 | Double d = Double.valueOf(string); 345 | if (!d.isInfinite() && !d.isNaN()) { 346 | return d; 347 | } 348 | } else { 349 | Long myLong = Long.valueOf(string); 350 | if (string.equals(myLong.toString())) { 351 | if (myLong.longValue() == myLong.intValue()) { 352 | return Integer.valueOf(myLong.intValue()); 353 | } 354 | return myLong; 355 | } 356 | } 357 | } catch (Exception ignore) { 358 | } 359 | } 360 | return string; 361 | } 362 | 363 | 364 | public static JSONObject toJSONObject(String string) throws JSONException { 365 | return toJSONObject(string, false); 366 | } 367 | 368 | 369 | public static JSONObject toJSONObject(Reader reader) throws JSONException { 370 | return toJSONObject(reader, false); 371 | } 372 | 373 | 374 | public static JSONObject toJSONObject(Reader reader, boolean keepStrings) throws JSONException { 375 | JSONObject jo = new JSONObject(); 376 | XMLTokener x = new XMLTokener(reader); 377 | while (x.more()) { 378 | x.skipPast("<"); 379 | if(x.more()) { 380 | parse(x, jo, null, keepStrings); 381 | } 382 | } 383 | return jo; 384 | } 385 | 386 | 387 | public static JSONObject toJSONObject(String string, boolean keepStrings) throws JSONException { 388 | return toJSONObject(new StringReader(string), keepStrings); 389 | } 390 | 391 | 392 | public static String toString(Object object) throws JSONException { 393 | return toString(object, null); 394 | } 395 | 396 | 397 | public static String toString(final Object object, final String tagName) 398 | throws JSONException { 399 | StringBuilder sb = new StringBuilder(); 400 | JSONArray ja; 401 | JSONObject jo; 402 | String string; 403 | 404 | if (object instanceof JSONObject) { 405 | 406 | 407 | if (tagName != null) { 408 | sb.append('<'); 409 | sb.append(tagName); 410 | sb.append('>'); 411 | } 412 | 413 | 414 | 415 | jo = (JSONObject) object; 416 | for (final String key : jo.keySet()) { 417 | Object value = jo.opt(key); 418 | if (value == null) { 419 | value = ""; 420 | } else if (value.getClass().isArray()) { 421 | value = new JSONArray(value); 422 | } 423 | 424 | 425 | if ("content".equals(key)) { 426 | if (value instanceof JSONArray) { 427 | ja = (JSONArray) value; 428 | int jaLength = ja.length(); 429 | 430 | for (int i = 0; i < jaLength; i++) { 431 | if (i > 0) { 432 | sb.append('\n'); 433 | } 434 | Object val = ja.opt(i); 435 | sb.append(escape(val.toString())); 436 | } 437 | } else { 438 | sb.append(escape(value.toString())); 439 | } 440 | 441 | 442 | 443 | } else if (value instanceof JSONArray) { 444 | ja = (JSONArray) value; 445 | int jaLength = ja.length(); 446 | 447 | for (int i = 0; i < jaLength; i++) { 448 | Object val = ja.opt(i); 449 | if (val instanceof JSONArray) { 450 | sb.append('<'); 451 | sb.append(key); 452 | sb.append('>'); 453 | sb.append(toString(val)); 454 | sb.append("'); 457 | } else { 458 | sb.append(toString(val, key)); 459 | } 460 | } 461 | } else if ("".equals(value)) { 462 | sb.append('<'); 463 | sb.append(key); 464 | sb.append("/>"); 465 | 466 | 467 | 468 | } else { 469 | sb.append(toString(value, key)); 470 | } 471 | } 472 | if (tagName != null) { 473 | 474 | 475 | sb.append("'); 478 | } 479 | return sb.toString(); 480 | 481 | } 482 | 483 | if (object != null && (object instanceof JSONArray || object.getClass().isArray())) { 484 | if(object.getClass().isArray()) { 485 | ja = new JSONArray(object); 486 | } else { 487 | ja = (JSONArray) object; 488 | } 489 | int jaLength = ja.length(); 490 | 491 | for (int i = 0; i < jaLength; i++) { 492 | Object val = ja.opt(i); 493 | 494 | 495 | 496 | sb.append(toString(val, tagName == null ? "array" : tagName)); 497 | } 498 | return sb.toString(); 499 | } 500 | 501 | string = (object == null) ? "null" : escape(object.toString()); 502 | return (tagName == null) ? "\"" + string + "\"" 503 | : (string.length() == 0) ? "<" + tagName + "/>" : "<" + tagName 504 | + ">" + string + ""; 505 | 506 | } 507 | } 508 | -------------------------------------------------------------------------------- /src/main/java/com/browserstack/json/XMLTokener.java: -------------------------------------------------------------------------------- 1 | package com.browserstack.json; 2 | 3 | 4 | 5 | import java.io.Reader; 6 | 7 | 8 | public class XMLTokener extends JSONTokener { 9 | 10 | 11 | 12 | public static final java.util.HashMap entity; 13 | 14 | static { 15 | entity = new java.util.HashMap(8); 16 | entity.put("amp", XML.AMP); 17 | entity.put("apos", XML.APOS); 18 | entity.put("gt", XML.GT); 19 | entity.put("lt", XML.LT); 20 | entity.put("quot", XML.QUOT); 21 | } 22 | 23 | 24 | public XMLTokener(Reader r) { 25 | super(r); 26 | } 27 | 28 | 29 | public XMLTokener(String s) { 30 | super(s); 31 | } 32 | 33 | 34 | public String nextCDATA() throws JSONException { 35 | char c; 36 | int i; 37 | StringBuilder sb = new StringBuilder(); 38 | while (more()) { 39 | c = next(); 40 | sb.append(c); 41 | i = sb.length() - 3; 42 | if (i >= 0 && sb.charAt(i) == ']' && 43 | sb.charAt(i + 1) == ']' && sb.charAt(i + 2) == '>') { 44 | sb.setLength(i); 45 | return sb.toString(); 46 | } 47 | } 48 | throw syntaxError("Unclosed CDATA"); 49 | } 50 | 51 | 52 | 53 | public Object nextContent() throws JSONException { 54 | char c; 55 | StringBuilder sb; 56 | do { 57 | c = next(); 58 | } while (Character.isWhitespace(c)); 59 | if (c == 0) { 60 | return null; 61 | } 62 | if (c == '<') { 63 | return XML.LT; 64 | } 65 | sb = new StringBuilder(); 66 | for (;;) { 67 | if (c == 0) { 68 | return sb.toString().trim(); 69 | } 70 | if (c == '<') { 71 | back(); 72 | return sb.toString().trim(); 73 | } 74 | if (c == '&') { 75 | sb.append(nextEntity(c)); 76 | } else { 77 | sb.append(c); 78 | } 79 | c = next(); 80 | } 81 | } 82 | 83 | 84 | 85 | public Object nextEntity(char ampersand) throws JSONException { 86 | StringBuilder sb = new StringBuilder(); 87 | for (;;) { 88 | char c = next(); 89 | if (Character.isLetterOrDigit(c) || c == '#') { 90 | sb.append(Character.toLowerCase(c)); 91 | } else if (c == ';') { 92 | break; 93 | } else { 94 | throw syntaxError("Missing ';' in XML entity: &" + sb); 95 | } 96 | } 97 | String string = sb.toString(); 98 | return unescapeEntity(string); 99 | } 100 | 101 | 102 | static String unescapeEntity(String e) { 103 | 104 | if (e == null || e.isEmpty()) { 105 | return ""; 106 | } 107 | 108 | if (e.charAt(0) == '#') { 109 | int cp; 110 | if (e.charAt(1) == 'x') { 111 | 112 | cp = Integer.parseInt(e.substring(2), 16); 113 | } else { 114 | 115 | cp = Integer.parseInt(e.substring(1)); 116 | } 117 | return new String(new int[] {cp},0,1); 118 | } 119 | Character knownEntity = entity.get(e); 120 | if(knownEntity==null) { 121 | 122 | return '&' + e + ';'; 123 | } 124 | return knownEntity.toString(); 125 | } 126 | 127 | 128 | 129 | public Object nextMeta() throws JSONException { 130 | char c; 131 | char q; 132 | do { 133 | c = next(); 134 | } while (Character.isWhitespace(c)); 135 | switch (c) { 136 | case 0: 137 | throw syntaxError("Misshaped meta tag"); 138 | case '<': 139 | return XML.LT; 140 | case '>': 141 | return XML.GT; 142 | case '/': 143 | return XML.SLASH; 144 | case '=': 145 | return XML.EQ; 146 | case '!': 147 | return XML.BANG; 148 | case '?': 149 | return XML.QUEST; 150 | case '"': 151 | case '\'': 152 | q = c; 153 | for (;;) { 154 | c = next(); 155 | if (c == 0) { 156 | throw syntaxError("Unterminated string"); 157 | } 158 | if (c == q) { 159 | return Boolean.TRUE; 160 | } 161 | } 162 | default: 163 | for (;;) { 164 | c = next(); 165 | if (Character.isWhitespace(c)) { 166 | return Boolean.TRUE; 167 | } 168 | switch (c) { 169 | case 0: 170 | case '<': 171 | case '>': 172 | case '/': 173 | case '=': 174 | case '!': 175 | case '?': 176 | case '"': 177 | case '\'': 178 | back(); 179 | return Boolean.TRUE; 180 | } 181 | } 182 | } 183 | } 184 | 185 | 186 | 187 | public Object nextToken() throws JSONException { 188 | char c; 189 | char q; 190 | StringBuilder sb; 191 | do { 192 | c = next(); 193 | } while (Character.isWhitespace(c)); 194 | switch (c) { 195 | case 0: 196 | throw syntaxError("Misshaped element"); 197 | case '<': 198 | throw syntaxError("Misplaced '<'"); 199 | case '>': 200 | return XML.GT; 201 | case '/': 202 | return XML.SLASH; 203 | case '=': 204 | return XML.EQ; 205 | case '!': 206 | return XML.BANG; 207 | case '?': 208 | return XML.QUEST; 209 | 210 | // Quoted string 211 | 212 | case '"': 213 | case '\'': 214 | q = c; 215 | sb = new StringBuilder(); 216 | for (;;) { 217 | c = next(); 218 | if (c == 0) { 219 | throw syntaxError("Unterminated string"); 220 | } 221 | if (c == q) { 222 | return sb.toString(); 223 | } 224 | if (c == '&') { 225 | sb.append(nextEntity(c)); 226 | } else { 227 | sb.append(c); 228 | } 229 | } 230 | default: 231 | 232 | // Name 233 | 234 | sb = new StringBuilder(); 235 | for (;;) { 236 | sb.append(c); 237 | c = next(); 238 | if (Character.isWhitespace(c)) { 239 | return sb.toString(); 240 | } 241 | switch (c) { 242 | case 0: 243 | return sb.toString(); 244 | case '>': 245 | case '/': 246 | case '=': 247 | case '!': 248 | case '?': 249 | case '[': 250 | case ']': 251 | back(); 252 | return sb.toString(); 253 | case '<': 254 | case '"': 255 | case '\'': 256 | throw syntaxError("Bad character in a name"); 257 | } 258 | } 259 | } 260 | } 261 | 262 | 263 | 264 | 265 | 266 | 267 | public void skipPast(String to) { 268 | boolean b; 269 | char c; 270 | int i; 271 | int j; 272 | int offset = 0; 273 | int length = to.length(); 274 | char[] circle = new char[length]; 275 | 276 | 277 | 278 | for (i = 0; i < length; i += 1) { 279 | c = next(); 280 | if (c == 0) { 281 | return; 282 | } 283 | circle[i] = c; 284 | } 285 | 286 | 287 | 288 | for (;;) { 289 | j = offset; 290 | b = true; 291 | 292 | 293 | 294 | for (i = 0; i < length; i += 1) { 295 | if (circle[j] != to.charAt(i)) { 296 | b = false; 297 | break; 298 | } 299 | j += 1; 300 | if (j >= length) { 301 | j -= length; 302 | } 303 | } 304 | 305 | 306 | 307 | if (b) { 308 | return; 309 | } 310 | 311 | 312 | 313 | c = next(); 314 | if (c == 0) { 315 | return; 316 | } 317 | 318 | circle[offset] = c; 319 | offset += 1; 320 | if (offset >= length) { 321 | offset -= length; 322 | } 323 | } 324 | } 325 | } 326 | -------------------------------------------------------------------------------- /src/main/resources/META-INF/gradle-plugins/com.browserstack.gradle-sdk.properties: -------------------------------------------------------------------------------- 1 | implementation-class=com.browserstack.gradle.BrowserStackSDKPlugin 2 | -------------------------------------------------------------------------------- /src/main/resources/META-INF/gradle-plugins/com.browserstack.gradle.properties: -------------------------------------------------------------------------------- 1 | implementation-class=com.browserstack.gradle.BrowserStackPlugin 2 | -------------------------------------------------------------------------------- /test.rb: -------------------------------------------------------------------------------- 1 | PWD=`pwd`.strip 2 | 3 | def run_command(s) 4 | stdout = `#{s} 2>&1` 5 | return stdout 6 | end 7 | 8 | def print_separator 9 | puts "\n******************************************************************************************************" 10 | end 11 | 12 | def setup_required_paths 13 | $current_path = $current_path.chop 14 | $correct_absolute_mainAPKPath = $current_path + "/test/mainApk" 15 | $incorrect_absolute_APKPath = "/random_path/" 16 | $correct_absolute_testAPKPath = $current_path + "/test/testApk" 17 | $correct_relative_mainAPKPath = "./test/mainApk" 18 | $incorrect_relative_APKPath = "./random_path/" 19 | $correct_relative_testAPKPath = "./test/testApk" 20 | end 21 | 22 | def setup_repo 23 | puts "Setting up sample repo" 24 | run_command("git clone https://github.com/browserstack/espresso-browserstack.git") 25 | Dir.chdir "espresso-browserstack" 26 | $current_path = run_command("pwd"); 27 | run_command("git checkout gradlePluginTestBranch") 28 | end 29 | 30 | def setup_repo_with_app_variants 31 | puts "Adding gradle file with flavors." 32 | run_command("rm app/build.gradle") 33 | run_command("mv app/build-with-flavours.gradle app/build.gradle") 34 | end 35 | 36 | def run_basic_espresso_test(gradle_command) 37 | puts "Running #{gradle_command} with basic config" 38 | stdout = run_command(gradle_command) 39 | responses = stdout.lines.select{ |line| line.match(/app_url|test_suite_url|build_id/)} 40 | if responses.count != 3 41 | puts "✘ #{gradle_command} failed with error: #{stdout}".red 42 | else 43 | puts "✔ #{gradle_command} tests passed".green 44 | puts responses.join("\n") 45 | end 46 | end 47 | 48 | def run_espresso_test_with_path(gradle_command) 49 | puts "Running #{gradle_command} with config and path to main and test apk" 50 | stdout = run_command(gradle_command) 51 | responses = stdout.lines.select{ |line| line.match(/app_url|test_suite_url|build_id/)} 52 | if responses.count != 3 53 | puts "✘ #{gradle_command} failed with error: #{responses}".red 54 | else 55 | puts "✔ #{gradle_command} tests passed".green 56 | puts responses.join("\n") 57 | end 58 | end 59 | 60 | def run_espresso_test_with_invalid_cucumber_config(gradle_command) 61 | puts "Running #{gradle_command} with invalid cucumber config and path to main and test apk" 62 | stdout = run_command(gradle_command) 63 | responses = stdout.lines.select{ |line| line.match(/app_url|test_suite_url|422|BROWSERSTACK_INVALID_VALUES_IN_INPUT/)} 64 | if responses.count != 4 65 | puts "✘ #{gradle_command} failed with error: #{responses}".red 66 | else 67 | puts "✔ #{gradle_command} tests passed".green 68 | puts responses.join("\n") 69 | end 70 | end 71 | 72 | def run_espresso_test_with_invalid_instrumentation_config(gradle_command) 73 | puts "Running #{gradle_command} with invalid instrumentation config and path to main and test apk" 74 | stdout = run_command(gradle_command) 75 | responses = stdout.lines.select{ |line| line.match(/app_url|test_suite_url|422|contain characters/)} 76 | if responses.count != 4 77 | puts "✘ #{gradle_command} failed with error: #{responses}".red 78 | else 79 | puts "✔ #{gradle_command} tests passed".green 80 | puts responses.join("\n") 81 | end 82 | end 83 | 84 | def run_espresso_test_with_incorrect_path(gradle_command) 85 | puts "Running #{gradle_command} with basic config and incorrect paths to both main and test apk" 86 | stdout = run_command(gradle_command) 87 | responses = stdout.lines.select{ |line| line.match(/DebugApp apk: null|TestApp apk: null/)} 88 | if responses.count != 2 89 | puts "✘ #{gradle_command} failed with error: #{responses}".red 90 | else 91 | puts "✔ #{gradle_command} tests passed".green 92 | puts responses.join("\n") 93 | end 94 | end 95 | 96 | def run_espresso_test_with_either_main_or_test_apk_path(gradle_command, apk_path) 97 | puts "Running #{gradle_command} with basic config and either main apk path or test apk path" 98 | stdout = run_command(gradle_command) 99 | responses = stdout.lines.select{ |line| line.match(/#{apk_path}|test_suite_url|build_id/)} 100 | if responses.count != 3 101 | puts "✘ #{gradle_command} failed with error: #{responses}".red 102 | else 103 | puts "✔ #{gradle_command} tests passed".green 104 | puts responses.join("\n") 105 | end 106 | end 107 | 108 | def run_espresso_test_with_one_absolute_and_one_relative_apk_path(gradle_command, main_apk_path, test_apk_path) 109 | puts "Running #{gradle_command} with basic config and one of the paths as absolute and one as relative" 110 | stdout = run_command(gradle_command) 111 | responses = stdout.lines.select{ |line| line.match(/#{main_apk_path}|#{test_apk_path}|test_suite_url|build_id/)} 112 | if responses.count != 4 113 | puts "✘ #{gradle_command} failed with error: #{responses}".red 114 | else 115 | puts "✔ #{gradle_command} tests passed".green 116 | puts responses.join("\n") 117 | end 118 | end 119 | 120 | def run_app_live_test(gradle_command) 121 | puts "Running #{gradle_command}" 122 | stdout = run_command(gradle_command) 123 | responses = stdout.lines.select{ |line| line.match(/app_url|test_suite_url|build_id/)} 124 | if responses.empty? 125 | puts "✘ #{gradle_command} failed with error: #{stdout}".red 126 | else 127 | puts "✔ #{gradle_command} tests passed".green 128 | puts responses.join("\n") 129 | end 130 | end 131 | 132 | def run_tests 133 | puts "\nRunning new tests using ./gradlew" 134 | run_basic_espresso_test("./gradlew clean executeDebugTestsOnBrowserstack") 135 | print_separator 136 | puts "\n" 137 | run_app_live_test("./gradlew clean uploadDebugToBrowserstackAppLive") 138 | print_separator 139 | end 140 | 141 | def run_tests_args 142 | puts "\nRunning new tests using ./gradlew with args" 143 | run_basic_espresso_test("./gradlew clean executeDebugTestsOnBrowserstack --config-file='command-line-config-browserstack.json'") 144 | run_basic_espresso_test("./gradlew executeDebugTestsOnBrowserstack -PskipBuildingApks=true") 145 | run_basic_espresso_test("./gradlew executeDebugTestsOnBrowserstack -PskipBuildingApks=false") 146 | print_separator 147 | end 148 | 149 | def run_tests_with_path_args 150 | puts "\nRunning new test using ./gradlew with APK paths for main and test apk" 151 | mainAPKPath = $current_path + "/test/mainApk" 152 | testAPKPAth = $current_path + "/test/testApk" 153 | run_espresso_test_with_path("./gradlew executeDebugTestsOnBrowserstack -PmainAPKPath=#{mainAPKPath} -PtestAPKPath=#{testAPKPAth}") 154 | end 155 | 156 | def run_tests_with_relative_path 157 | puts "\nRunning new test using ./gradlew with relative paths for both main and test apk" 158 | mainAPKPath = "./test/mainApk" 159 | testAPKPAth = "./test/testApk" 160 | run_espresso_test_with_path("./gradlew executeDebugTestsOnBrowserstack -PmainAPKPath=#{mainAPKPath} -PtestAPKPath=#{testAPKPAth}") 161 | end 162 | 163 | def run_test_with_incorrect_path 164 | puts "\nRunning new test using ./gradlew with incorrect main and test APK paths" 165 | mainAPKPath = $current_path 166 | testAPKPAth = $current_path 167 | run_espresso_test_with_incorrect_path("./gradlew executeDebugTestsOnBrowserstack -PmainAPKPath=#{mainAPKPath} -PtestAPKPath=#{testAPKPAth}") 168 | end 169 | 170 | def run_test_with_incorrect_relative_path 171 | puts "\nRunning new test using ./gradlew with incorrect relative paths for both main and test APK " 172 | mainAPKPath = "./test" 173 | testAPKPAth = "./test" 174 | run_espresso_test_with_incorrect_path("./gradlew executeDebugTestsOnBrowserstack -PmainAPKPath=#{mainAPKPath} -PtestAPKPath=#{testAPKPAth}") 175 | end 176 | 177 | 178 | def run_tests_with_path_variations 179 | puts "\nRunning new test using ./gradlew with mainAPKPath arg only" 180 | mainAPKPath = $current_path + "/test/mainApk" 181 | run_espresso_test_with_either_main_or_test_apk_path("./gradlew executeDebugTestsOnBrowserstack -PmainAPKPath=#{mainAPKPath}", mainAPKPath); 182 | puts "\nRunning new test using ./gradlew with testAPKPath arg only" 183 | testAPKPAth = $current_path + "/test/testApk" 184 | run_espresso_test_with_either_main_or_test_apk_path("./gradlew executeDebugTestsOnBrowserstack -PtestAPKPath=#{testAPKPAth}", testAPKPAth); 185 | end 186 | 187 | def run_tests_with_relative_path_variations 188 | puts "\nRunning new test using ./gradlew with mainAPKPath(relative path) arg only" 189 | mainAPKPath = "./test/mainApk" 190 | run_espresso_test_with_either_main_or_test_apk_path("./gradlew executeDebugTestsOnBrowserstack -PmainAPKPath=#{mainAPKPath}", mainAPKPath); 191 | puts "\nRunning new test using ./gradlew with testAPKPath(relative path) arg only" 192 | testAPKPAth = "./test/testApk" 193 | run_espresso_test_with_either_main_or_test_apk_path("./gradlew executeDebugTestsOnBrowserstack -PtestAPKPath=#{testAPKPAth}", testAPKPAth); 194 | puts "\nRunning with absolute main and relative test path" 195 | mainAPKPath = $current_path + "/test/mainApk" 196 | run_espresso_test_with_one_absolute_and_one_relative_apk_path("./gradlew executeDebugTestsOnBrowserstack -PmainAPKPath=#{mainAPKPath} -PtestAPKPath=#{testAPKPAth}", mainAPKPath, testAPKPAth) 197 | puts "\nRunning with absolute test and relative main path" 198 | testAPKPAth = $current_path + "/test/testApk" 199 | mainAPKPath = "./test/mainApk" 200 | run_espresso_test_with_one_absolute_and_one_relative_apk_path("./gradlew executeDebugTestsOnBrowserstack -PmainAPKPath=#{mainAPKPath} -PtestAPKPath=#{testAPKPAth}", mainAPKPath, testAPKPAth) 201 | end 202 | 203 | def run_espresso_test_with_non_existing_apk_path(gradle_command, apk_type) 204 | puts "Running #{gradle_command} with non existing path" 205 | stdout = run_command(gradle_command) 206 | responses = stdout.lines.select{ |line| line.match(/Invalid File Path: Please provide a valid #{apk_type} APK path/)} 207 | if responses.count != 1 208 | puts "✘ #{gradle_command} failed with error: #{responses}".red 209 | else 210 | puts "✔ #{gradle_command} tests passed".green 211 | puts responses.join("\n") 212 | end 213 | end 214 | 215 | def run_espresso_test_with_one_existing_and_one_non_existing_apk_path(gradle_command, incorrect_apk_type) 216 | puts "Running #{gradle_command}" 217 | stdout = run_command(gradle_command) 218 | response_line_with_incorrect_path = stdout.lines.select{ |line| line.match(/Invalid File Path: Please provide a valid #{incorrect_apk_type} APK path/)} 219 | response_line_with_build_id = stdout.lines.select{ |line| line.match(/build_id/)} 220 | if response_line_with_incorrect_path.count != 1 && response_line_with_build_id != 0 221 | puts "✘ #{gradle_command} failed with error: #{response_line_with_incorrect_path}".red 222 | else 223 | puts "✔ #{gradle_command} tests passed".green 224 | puts response_line_with_incorrect_path.join("\n") 225 | end 226 | end 227 | 228 | def run_espresso_test_with_one_incorrect_apk_path(gradle_command) 229 | puts "Running #{gradle_command}" 230 | stdout = run_command(gradle_command) 231 | responses = stdout.lines.select{ |line| line.match(/TestApp apk: null/)} 232 | if responses.count != 1 233 | puts "✘ #{gradle_command} failed with error: #{responses}".red 234 | else 235 | puts "✔ #{gradle_command} tests passed".green 236 | puts responses.join("\n") 237 | end 238 | end 239 | 240 | def run_test_with_non_existing_path 241 | puts "\nRunning new test with non existing absolute main path" 242 | mainAPKPath = "/random_path/" 243 | run_espresso_test_with_non_existing_apk_path("./gradlew executeDebugTestsOnBrowserstack -PmainAPKPath=#{mainAPKPath}", "main"); 244 | puts "\nRunning new test with non existing relative main path" 245 | mainAPKPath = "./random_path/" 246 | run_espresso_test_with_non_existing_apk_path("./gradlew executeDebugTestsOnBrowserstack -PmainAPKPath=#{mainAPKPath}", "main"); 247 | puts "\nRunning new test with non existing absolute test path" 248 | testAPKPath = "/random_path/" 249 | run_espresso_test_with_non_existing_apk_path("./gradlew executeDebugTestsOnBrowserstack -PtestAPKPath=#{testAPKPath}", "test"); 250 | puts "\nRunning new test with non existing absolute test path" 251 | testAPKPath = "./random_path/" 252 | run_espresso_test_with_non_existing_apk_path("./gradlew executeDebugTestsOnBrowserstack -PtestAPKPath=#{testAPKPath}", "test"); 253 | end 254 | 255 | def run_test_with_one_correct_and_one_non_existing_path 256 | puts "\nRunning new test with absolute correct test and non existing absolute main path" 257 | run_espresso_test_with_one_existing_and_one_non_existing_apk_path("./gradlew executeDebugTestsOnBrowserstack -PmainAPKPath=#{$incorrect_absolute_APKPath} -PtestAPKPath=#{$correct_absolute_testAPKPath}", "main"); 258 | puts "\nRunning new test with relative correct test and non existing absolute main path" 259 | run_espresso_test_with_one_existing_and_one_non_existing_apk_path("./gradlew executeDebugTestsOnBrowserstack -PmainAPKPath=#{$incorrect_absolute_APKPath} -PtestAPKPath=#{$correct_relative_testAPKPath}", "main"); 260 | puts "\nRunning new test with absolute correct test and non existing relative main path" 261 | run_espresso_test_with_one_existing_and_one_non_existing_apk_path("./gradlew executeDebugTestsOnBrowserstack -PmainAPKPath=#{$incorrect_relative_APKPath} -PtestAPKPath=#{$correct_absolute_testAPKPath}", "main"); 262 | puts "\nRunning new test with relative correct test and non existing relative main path" 263 | run_espresso_test_with_one_existing_and_one_non_existing_apk_path("./gradlew executeDebugTestsOnBrowserstack -PmainAPKPath=#{$incorrect_relative_APKPath} -PtestAPKPath=#{$correct_relative_testAPKPath}", "main"); 264 | puts "\nRunning new test with absolute correct main and non existing absolute test path" 265 | run_espresso_test_with_one_existing_and_one_non_existing_apk_path("./gradlew executeDebugTestsOnBrowserstack -PmainAPKPath=#{$correct_absolute_mainAPKPath} -PtestAPKPath=#{$incorrect_absolute_APKPath}", "test"); 266 | puts "\nRunning new test with absolute correct main and non existing relative test path" 267 | run_espresso_test_with_one_existing_and_one_non_existing_apk_path("./gradlew executeDebugTestsOnBrowserstack -PmainAPKPath=#{$correct_absolute_mainAPKPath} -PtestAPKPath=#{$incorrect_relative_APKPath}", "test"); 268 | puts "\nRunning new test with relative correct main and non existing absolute test path" 269 | run_espresso_test_with_one_existing_and_one_non_existing_apk_path("./gradlew executeDebugTestsOnBrowserstack -PmainAPKPath=#{$correct_relative_mainAPKPath} -PtestAPKPath=#{$incorrect_absolute_APKPath}", "test"); 270 | puts "\nRunning new test with relative correct main and non existing relative test path" 271 | run_espresso_test_with_one_existing_and_one_non_existing_apk_path("./gradlew executeDebugTestsOnBrowserstack -PmainAPKPath=#{$correct_relative_mainAPKPath} -PtestAPKPath=#{$incorrect_relative_APKPath}", "test"); 272 | end 273 | 274 | def run_test_with_ipa 275 | puts "Running tests with ipa files" 276 | mainAPKPath = $current_path + "/test/mainApk/ipa" 277 | testAPKPAth = $current_path + "/test/testApk/ipa" 278 | run_espresso_test_with_incorrect_path("./gradlew executeDebugTestsOnBrowserstack -PmainAPKPath=#{mainAPKPath} -PtestAPKPath=#{testAPKPAth}") 279 | end 280 | 281 | def run_test_with_zip 282 | puts "Running tests with ipa files" 283 | mainAPKPath = $current_path + "/test/mainApk/" 284 | testAPKPAth = $current_path + "/test/mainApk/zip" 285 | run_espresso_test_with_one_incorrect_apk_path("./gradlew executeDebugTestsOnBrowserstack -PmainAPKPath=#{mainAPKPath} -PtestAPKPath=#{testAPKPAth}"); 286 | end 287 | 288 | def run_tests_with_flavors 289 | puts "Running tests with flavors using ./gradlew" 290 | run_basic_espresso_test("./gradlew clean executePhoneDebugTestsOnBrowserstack") 291 | print_separator 292 | puts "\n" 293 | run_app_live_test("./gradlew clean uploadPhoneDebugToBrowserstackAppLive") 294 | print_separator 295 | end 296 | 297 | def remove_repo 298 | Dir.chdir PWD 299 | run_command("rm -rf espresso-browserstack") 300 | end 301 | 302 | def build_plugin 303 | puts "Building gradle plugin using ./gradlew" 304 | run_command("./gradlew clean build") 305 | end 306 | 307 | def validate_env 308 | missing_env_variables = [] 309 | requied_env_variables = ["ANDROID_HOME","BROWSERSTACK_USERNAME", "BROWSERSTACK_ACCESS_KEY"] 310 | requied_env_variables.each do |env_variable| 311 | if ENV[env_variable].nil? 312 | missing_env_variables += [env_variable] 313 | end 314 | end 315 | if !missing_env_variables.empty? 316 | raise "Please export #{missing_env_variables.join(',')}" 317 | end 318 | end 319 | 320 | 321 | def run_cli_tests 322 | puts "\nRunning CLI tests using ./gradlew with args" 323 | run_cli_test_help_command("./gradlew browserstackCLIWrapper -Pcommand='help'"); 324 | run_cli_test_delete_command("./gradlew browserstackCLIWrapper -Pcommand='app-automate apps delete -a bs://3fc4eea395ea6efc69b74e8211ecf2eba8879373'") 325 | print_separator 326 | end 327 | 328 | def run_cli_test_help_command(gradle_command) 329 | puts "Running #{gradle_command}" 330 | stdout = run_command(gradle_command) 331 | responses = stdout.lines.select{ |line| line.match(/BrowserStack-cli|authenticate|app-automate/)} 332 | if responses.count != 3 333 | puts "✘ #{gradle_command} failed with error: #{stdout}".red 334 | else 335 | puts "✔ #{gradle_command} tests passed".green 336 | puts responses.join("\n") 337 | end 338 | end 339 | 340 | def run_cli_test_delete_command(gradle_command) 341 | puts "Running #{gradle_command}" 342 | stdout = run_command(gradle_command) 343 | responses = stdout.lines.select{ |line| line.match(/BROWSERSTACK_APP_NOT_FOUND/)} 344 | if responses.count != 1 345 | puts "✘ #{gradle_command} failed with error: #{stdout}".red 346 | else 347 | puts "✔ #{gradle_command} tests passed".green 348 | puts responses.join("\n") 349 | end 350 | end 351 | 352 | def run_test_with_cucumber_options 353 | puts "\nRunning new test using ./gradlew with absolute APK paths with cucumber options" 354 | mainAPKPath = $current_path + "/test/mainApk/cucumber" 355 | testAPKPAth = $current_path + "/test/testApk/cucumber" 356 | run_espresso_test_with_path("./gradlew executeDebugTestsOnBrowserstack -PmainAPKPath=#{mainAPKPath} -PtestAPKPath=#{testAPKPAth} --config-file=config-browserstack_cucumber.json") 357 | puts "\nRunning new test using ./gradlew with absolute APK paths with invalid cucumber options" 358 | run_espresso_test_with_invalid_cucumber_config("./gradlew executeDebugTestsOnBrowserstack -PmainAPKPath=#{mainAPKPath} -PtestAPKPath=#{testAPKPAth} --config-file=config-browserstack_cucumber_invalid_name.json") 359 | puts "\nRunning new test using ./gradlew with relative APK paths with cucumber options" 360 | mainAPKPath = "./test/mainApk/cucumber" 361 | testAPKPAth = "./test/testApk/cucumber" 362 | run_espresso_test_with_path("./gradlew executeDebugTestsOnBrowserstack -PmainAPKPath=#{mainAPKPath} -PtestAPKPath=#{testAPKPAth} --config-file=config-browserstack_cucumber.json") 363 | puts "\nRunning new test using ./gradlew with relative APK paths with invalid cucumber options" 364 | run_espresso_test_with_invalid_cucumber_config("./gradlew executeDebugTestsOnBrowserstack -PmainAPKPath=#{mainAPKPath} -PtestAPKPath=#{testAPKPAth} --config-file=config-browserstack_cucumber_invalid_name.json") 365 | end 366 | 367 | def run_test_with_instrumentation_options 368 | puts "\nRunning new test using ./gradlew with absolute APK paths with instrumentation options" 369 | mainAPKPath = $current_path + "/test/mainApk/cucumber" 370 | testAPKPAth = $current_path + "/test/testApk/cucumber" 371 | run_espresso_test_with_path("./gradlew executeDebugTestsOnBrowserstack -PmainAPKPath=#{mainAPKPath} -PtestAPKPath=#{testAPKPAth} --config-file=config-browserstack_instrumentation.json") 372 | puts "\nRunning new test using ./gradlew with absolute APK paths with invalid instrumentation options" 373 | run_espresso_test_with_invalid_instrumentation_config("./gradlew executeDebugTestsOnBrowserstack -PmainAPKPath=#{mainAPKPath} -PtestAPKPath=#{testAPKPAth} --config-file=config-browserstack_instrumentation_invalid.json") 374 | puts "\nRunning new test using ./gradlew with relative APK paths with instrumentation options" 375 | mainAPKPath = "./test/mainApk/cucumber" 376 | testAPKPAth = "./test/testApk/cucumber" 377 | run_espresso_test_with_path("./gradlew executeDebugTestsOnBrowserstack -PmainAPKPath=#{mainAPKPath} -PtestAPKPath=#{testAPKPAth} --config-file=config-browserstack_instrumentation.json") 378 | puts "\nRunning new test using ./gradlew with relative APK paths with invalid instrumentation options" 379 | run_espresso_test_with_invalid_instrumentation_config("./gradlew executeDebugTestsOnBrowserstack -PmainAPKPath=#{mainAPKPath} -PtestAPKPath=#{testAPKPAth} --config-file=config-browserstack_instrumentation_invalid.json") 380 | end 381 | 382 | def test 383 | validate_env 384 | build_plugin 385 | setup_repo 386 | setup_required_paths 387 | run_tests 388 | remove_repo 389 | setup_repo 390 | setup_required_paths 391 | run_tests_args 392 | run_tests_with_path_args 393 | run_test_with_incorrect_path 394 | run_tests_with_path_variations 395 | run_tests_with_relative_path 396 | run_test_with_incorrect_relative_path 397 | run_tests_with_relative_path_variations 398 | run_test_with_non_existing_path 399 | run_test_with_one_correct_and_one_non_existing_path 400 | run_test_with_ipa 401 | run_test_with_zip 402 | run_test_with_cucumber_options 403 | run_test_with_instrumentation_options 404 | run_cli_tests 405 | setup_repo_with_app_variants 406 | run_tests_with_flavors 407 | remove_repo 408 | end 409 | 410 | class String 411 | def red 412 | "\e[#{31}m#{self}\e[0m" 413 | end 414 | 415 | def green 416 | "\e[#{32}m#{self}\e[0m" 417 | end 418 | end 419 | 420 | test 421 | --------------------------------------------------------------------------------