diff --git a/.azure-pipelines/buildAndPackage.yml b/.azure-pipelines/buildAndPackage.yml index 6eaed042f..76beadda8 100644 --- a/.azure-pipelines/buildAndPackage.yml +++ b/.azure-pipelines/buildAndPackage.yml @@ -9,15 +9,16 @@ trigger: - dev - main - master + - feature/v2 paths: include: - src/* exclude: + - .gradle/wrapper - .gitignore - CONTRIBUTING.md - LICENSE - THIRD PARTY NOTICES - - build.gradle - gradle.properties - gradlew - gradlew.bat @@ -28,78 +29,12 @@ trigger: pr: none pool: - vmImage: 'windows-latest' + vmImage: windows-latest steps: -- checkout: self - clean: true - fetchDepth: 1 - -- task: securedevelopmentteam.vss-secure-development-tools.build-task-credscan.CredScan@2 - displayName: 'Run CredScan' - inputs: - debugMode: false - -- task: DownloadSecureFile@1 - inputs: - secureFile: 'local.properties' - -- task: DownloadSecureFile@1 - inputs: - secureFile: 'secring.gpg' - -- task: DownloadSecureFile@1 - inputs: - secureFile: 'secring.gpg.lock' - -- task: CopyFiles@2 - inputs: - SourceFolder: '$(Agent.TempDirectory)' - Contents: '**' - TargetFolder: '$(System.DefaultWorkingDirectory)' - -- task: Gradle@2 - inputs: - gradleWrapperFile: 'gradlew' - tasks: 'build' - publishJUnitResults: true - testResultsFiles: '**/TEST-*.xml' - javaHomeOption: 'JDKVersion' - sonarQubeRunAnalysis: false - -- task: PublishBuildArtifacts@1 - displayName: 'Publish Artifact: drop' - inputs: - PathtoPublish: '$(Build.ArtifactStagingDirectory)' - -- task: CopyFiles@2 - inputs: - SourceFolder: '$(System.DefaultWorkingDirectory)' - Contents: | - **/libs/* - build/generated-pom.xml - build/generated-pom.xml.asc - build.gradle - gradlew - gradlew.bat - settings.gradle - gradle.properties - **/gradle/wrapper/* - Scripts/** - TargetFolder: '$(Build.ArtifactStagingDirectory)/' - -- task: PublishBuildArtifacts@1 - inputs: - PathtoPublish: '$(Build.ArtifactStagingDirectory)' - ArtifactName: 'drop' - publishLocation: 'Container' - -- task: YodLabs.O365PostMessage.O365PostMessageBuild.O365PostMessageBuild@0 - displayName: 'Graph Client Tooling pipeline fail notification' - inputs: - addressType: serviceEndpoint - serviceEndpointName: 'microsoftgraph pipeline status' - title: '$(Build.DefinitionName) failure notification' - text: 'This pipeline has failed. View the build details for further information. This is a blocking failure.' - condition: and(failed(), ne(variables['Build.Reason'], 'Manual')) - enabled: true +- template: templates/checkout-and-credscan.yml +- template: templates/install-java.yml +- template: templates/secure-files.yml +- template: templates/build-and-coverage.yml +- template: templates/publish-artefacts.yml +- template: templates/alert-failure.yml diff --git a/.azure-pipelines/prValidate.yml b/.azure-pipelines/prValidate.yml index cf2bd540d..4a486e925 100644 --- a/.azure-pipelines/prValidate.yml +++ b/.azure-pipelines/prValidate.yml @@ -6,12 +6,12 @@ pr: branches: include: - - master - - main - dev + - main + - master + - feature/v2 paths: exclude: - - .azure-pipelines - .gradle/wrapper - .gitignore - CONTRIBUTING.md @@ -26,38 +26,11 @@ pr: trigger: none # disable triggers based on commits. -variables: - PACKAGE_NAME: 'microsoft-graph-core' - PROPERTIES_PATH: '.\gradle.properties' - -pool: +pool: vmImage: windows-latest steps: -- checkout: self - clean: true - fetchDepth: 1 - -- task: securedevelopmentteam.vss-secure-development-tools.build-task-credscan.CredScan@2 - displayName: 'Run CredScan' - inputs: - debugMode: false - -- task: Gradle@2 - inputs: - gradleWrapperFile: 'gradlew' - tasks: 'build' - publishJUnitResults: true - testResultsFiles: '**/TEST-*.xml' - javaHomeOption: 'JDKVersion' - sonarQubeRunAnalysis: false - -- task: YodLabs.O365PostMessage.O365PostMessageBuild.O365PostMessageBuild@0 - displayName: 'Graph Client Tooling pipeline fail notification' - inputs: - addressType: serviceEndpoint - serviceEndpointName: 'microsoftgraph pipeline status' - title: '$(Build.DefinitionName) failure notification' - text: 'This pipeline has failed. View the build details for further information. This is a blocking failure. ' - condition: and(failed(), ne(variables['Build.Reason'], 'Manual')) - enabled: true +- template: templates/checkout-and-credscan.yml +- template: templates/install-java.yml +- template: templates/build-and-coverage.yml +- template: templates/alert-failure.yml diff --git a/.azure-pipelines/templates/alert-failure.yml b/.azure-pipelines/templates/alert-failure.yml new file mode 100644 index 000000000..51f2043d0 --- /dev/null +++ b/.azure-pipelines/templates/alert-failure.yml @@ -0,0 +1,10 @@ +steps: +- task: YodLabs.O365PostMessage.O365PostMessageBuild.O365PostMessageBuild@0 + displayName: 'Graph Client Tooling pipeline fail notification' + inputs: + addressType: serviceEndpoint + serviceEndpointName: 'microsoftgraph pipeline status' + title: '$(Build.DefinitionName) failure notification' + text: 'This pipeline has failed. View the build details for further information. This is a blocking failure. ' + condition: and(failed(), ne(variables['Build.Reason'], 'Manual')) + enabled: true \ No newline at end of file diff --git a/.azure-pipelines/templates/build-and-coverage.yml b/.azure-pipelines/templates/build-and-coverage.yml new file mode 100644 index 000000000..9a56fac54 --- /dev/null +++ b/.azure-pipelines/templates/build-and-coverage.yml @@ -0,0 +1,16 @@ +steps: +- task: Gradle@2 + inputs: + gradleWrapperFile: 'gradlew' + tasks: 'build' + publishJUnitResults: true + testResultsFiles: '**/TEST-*.xml' + javaHomeOption: 'JDKVersion' + sonarQubeRunAnalysis: false + +- task: PublishCodeCoverageResults@1 + inputs: + codeCoverageTool: 'JaCoCo' + summaryFileLocation: $(System.DefaultWorkingDirectory)/build/reports/jacoco/test/jacocoTestReport.xml + pathToSources: $(System.DefaultWorkingDirectory)/src/main/java + failIfCoverageEmpty: true \ No newline at end of file diff --git a/.azure-pipelines/templates/checkout-and-credscan.yml b/.azure-pipelines/templates/checkout-and-credscan.yml new file mode 100644 index 000000000..cc5c7c2f5 --- /dev/null +++ b/.azure-pipelines/templates/checkout-and-credscan.yml @@ -0,0 +1,9 @@ +steps: +- checkout: self + clean: true + fetchDepth: 1 + +- task: securedevelopmentteam.vss-secure-development-tools.build-task-credscan.CredScan@2 + displayName: 'Run CredScan' + inputs: + debugMode: false \ No newline at end of file diff --git a/.azure-pipelines/templates/install-java.yml b/.azure-pipelines/templates/install-java.yml new file mode 100644 index 000000000..d739611a5 --- /dev/null +++ b/.azure-pipelines/templates/install-java.yml @@ -0,0 +1,11 @@ +steps: +- pwsh: 'Invoke-WebRequest -Uri https://github.com/AdoptOpenJDK/openjdk15-binaries/releases/download/jdk-15.0.2%2B7/OpenJDK15U-jdk_x64_windows_hotspot_15.0.2_7.zip -OutFile OpenJDK.zip' + displayName: 'Download JDK' + +- task: JavaToolInstaller@0 + inputs: + versionSpec: '15' + jdkArchitectureOption: 'x64' + jdkSourceOption: 'LocalDirectory' + jdkFile: 'OpenJDK.zip' + jdkDestinationDirectory: 'binaries' \ No newline at end of file diff --git a/.azure-pipelines/templates/publish-artefacts.yml b/.azure-pipelines/templates/publish-artefacts.yml new file mode 100644 index 000000000..5715208b2 --- /dev/null +++ b/.azure-pipelines/templates/publish-artefacts.yml @@ -0,0 +1,21 @@ +steps: +- task: CopyFiles@2 + inputs: + SourceFolder: '$(System.DefaultWorkingDirectory)' + Contents: | + **/libs/* + build/generated-pom.xml + build/generated-pom.xml.asc + build.gradle + gradlew + gradlew.bat + settings.gradle + gradle.properties + **/gradle/** + Scripts/** + TargetFolder: '$(Build.ArtifactStagingDirectory)/' + +- task: PublishBuildArtifacts@1 + displayName: 'Publish Artifact: drop' + inputs: + PathtoPublish: '$(Build.ArtifactStagingDirectory)' \ No newline at end of file diff --git a/.azure-pipelines/templates/secure-files.yml b/.azure-pipelines/templates/secure-files.yml new file mode 100644 index 000000000..0cdeb85b9 --- /dev/null +++ b/.azure-pipelines/templates/secure-files.yml @@ -0,0 +1,18 @@ +steps: +- task: DownloadSecureFile@1 + inputs: + secureFile: 'local.properties' + +- task: DownloadSecureFile@1 + inputs: + secureFile: 'secring.gpg' + +- task: DownloadSecureFile@1 + inputs: + secureFile: 'secring.gpg.lock' + +- task: CopyFiles@2 + inputs: + SourceFolder: '$(Agent.TempDirectory)' + Contents: '**' + TargetFolder: '$(System.DefaultWorkingDirectory)' \ No newline at end of file diff --git a/.github/workflows/api-level-lint.yml b/.github/workflows/api-level-lint.yml new file mode 100644 index 000000000..b576a559c --- /dev/null +++ b/.github/workflows/api-level-lint.yml @@ -0,0 +1,37 @@ +name: "Checks the SDK only using APIs from the targeted API level" + +on: + push: + branches: + - dev + - master + pull_request: + branches: + - dev + - master + - feature/v2 + +jobs: + lint-api-level: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-java@v1 + with: + java-version: 15 + - name: Setup Android SDK + uses: android-actions/setup-android@v2 + - name: Add execution right to the script + run: chmod +x gradlew + working-directory: ./android + - name: Build SDK with Android project configuration + id: lint + run: ./gradlew --no-daemon build + working-directory: ./android + - name: Upload linting results + if: failure() && steps.lint.outcome == 'failure' + uses: actions/upload-artifact@v2 + with: + name: lint-report + path: ./android/build/reports + diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index acbe219cf..270ccb4ef 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -22,6 +22,10 @@ jobs: # a pull request then we can checkout the head. fetch-depth: 2 + - uses: actions/setup-java@v1 + with: + java-version: 15 + # If this run was triggered by a pull request event, then checkout # the head of the pull request instead of the merge commit. - run: git checkout HEAD^2 diff --git a/.github/workflows/sample-build-check.yml b/.github/workflows/sample-build-check.yml new file mode 100644 index 000000000..59fc25902 --- /dev/null +++ b/.github/workflows/sample-build-check.yml @@ -0,0 +1,35 @@ +name: "samples build check" + +on: + push: + paths: + - '**/*.java' + - '**/*.yml' + +jobs: + samples-build-check-device-code: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-java@v1 + with: + java-version: 15 + - name: Add execution right to the script + run: chmod +x gradlew + working-directory: ./samples/deviceCodeSample + - name: Build and run type summary project + run: ./gradlew --no-daemon build + working-directory: ./samples/deviceCodeSample + samples-build-check-interactive: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-java@v1 + with: + java-version: 15 + - name: Add execution right to the script + run: chmod +x gradlew + working-directory: ./samples/interactiveBrowserSample + - name: Build and run type summary project + run: ./gradlew --no-daemon build + working-directory: ./samples/interactiveBrowserSample \ No newline at end of file diff --git a/.gitignore b/.gitignore index e34a1d606..26e2d94d5 100644 --- a/.gitignore +++ b/.gitignore @@ -29,6 +29,10 @@ hs_err_pid* .classpath .settings +# IntelliJ +*.iml +.idea/ + # Maven /target/ local.properties \ No newline at end of file diff --git a/android/.gitignore b/android/.gitignore new file mode 100644 index 000000000..f06dfad69 --- /dev/null +++ b/android/.gitignore @@ -0,0 +1,2 @@ +.gradle +build \ No newline at end of file diff --git a/android/AndroidManifest.xml b/android/AndroidManifest.xml new file mode 100644 index 000000000..da53f5433 --- /dev/null +++ b/android/AndroidManifest.xml @@ -0,0 +1,24 @@ + + + + + + + + + + \ No newline at end of file diff --git a/android/build.gradle b/android/build.gradle new file mode 100644 index 000000000..c7fc41b55 --- /dev/null +++ b/android/build.gradle @@ -0,0 +1,71 @@ +buildscript { + repositories { + google() + gradlePluginPortal() + maven { + url "https://plugins.gradle.org/m2/" + } + } + + dependencies { + classpath "com.gradle:gradle-enterprise-gradle-plugin:3.5" + classpath "com.android.tools.build:gradle:4.0.1" + classpath "com.github.ben-manes:gradle-versions-plugin:0.36.0" + } +} + +repositories { + google() + gradlePluginPortal() +} + +apply plugin: "com.android.library" +apply plugin: "com.github.ben-manes.versions" + +android { + compileSdkVersion 30 + + defaultConfig { + versionCode 1 + versionName "1.0" + minSdkVersion 26 + targetSdkVersion 30 + } + + buildTypes { + release { + minifyEnabled false + } + } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + lintOptions { + textOutput "stdout" + checkAllWarnings true + warningsAsErrors true + disable "UnusedResources" // Unused will be removed on release + disable "IconExpectedSize" // Using the material icons provided from Google + disable "GoogleAppIndexingApiWarning" // We might want to index our app later + disable "InvalidPackage" // Butterknife, Okio and Realm + disable "ResourceType" // Annotation binding + disable "GradleDependency" + disable "NewerVersionAvailable" + disable "DuplicatePlatformClasses" // xpp3 added by azure-identity + } + sourceSets { + main { + java.srcDirs = ['../src/main/java'] + res.srcDirs = ['../src/main/java'] + manifest.srcFile 'AndroidManifest.xml' + } + androidTest { + setRoot '../src/test' + } + } +} + +apply from: "../gradle/dependencies.gradle" diff --git a/android/gradle.properties b/android/gradle.properties new file mode 100644 index 000000000..942b6ca4f --- /dev/null +++ b/android/gradle.properties @@ -0,0 +1,43 @@ +# Project-wide Gradle settings. + +# IDE users: +# Settings specified in this file will override any Gradle settings +# configured through the IDE. + +# For more details on how to configure your build environment visit +# http://www.gradle.org/docs/current/userguide/build_environment.html + +# Specifies the JVM arguments used for the daemon process. +# The setting is particularly useful for tweaking memory settings. +# Default value: -Xmx10248m -XX:MaxPermSize=256m +# org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 + +# When configured, Gradle will run in incubating parallel mode. +# This option should only be used with decoupled projects. More details, visit +# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects +# org.gradle.parallel=true + +# The size of the library demands a large amount of RAM to build. Increase as necessary if you get GC errors +## linux requires 10G, OSX requires 11G +org.gradle.jvmargs=-XX:MaxPermSize=512m -Xmx2g +org.gradle.parallel=true +org.gradle.caching=true + +mavenGroupId = com.microsoft.graph +mavenArtifactId = microsoft-graph-core +mavenMajorVersion = 2 +mavenMinorVersion = 3 +mavenPatchVersion = 1 +mavenArtifactSuffix = + +#These values are used to run functional tests +#If you wish to run the functional tests, edit the gradle.properties +#file in your user directory instead of adding them here. +#ex: C:\Users\username\.gradle\gradle.properties +ClientId="CLIENT_ID" +Username="USERNAME" +Password="PASSWORD" + +#enable mavenCentralPublishingEnabled to publish to maven central +mavenCentralSnapshotArtifactSuffix = -SNAPSHOT +mavenCentralPublishingEnabled=false diff --git a/android/gradle/wrapper/gradle-wrapper.jar b/android/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 000000000..e708b1c02 Binary files /dev/null and b/android/gradle/wrapper/gradle-wrapper.jar differ diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 000000000..28ff446a2 --- /dev/null +++ b/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-6.8.1-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/android/gradlew b/android/gradlew new file mode 100644 index 000000000..4f906e0c8 --- /dev/null +++ b/android/gradlew @@ -0,0 +1,185 @@ +#!/usr/bin/env sh + +# +# Copyright 2015 the original author or authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin or MSYS, switch paths to Windows format before running java +if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=`expr $i + 1` + done + case $i in + 0) set -- ;; + 1) set -- "$args0" ;; + 2) set -- "$args0" "$args1" ;; + 3) set -- "$args0" "$args1" "$args2" ;; + 4) set -- "$args0" "$args1" "$args2" "$args3" ;; + 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=`save "$@"` + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +exec "$JAVACMD" "$@" diff --git a/android/gradlew.bat b/android/gradlew.bat new file mode 100644 index 000000000..107acd32c --- /dev/null +++ b/android/gradlew.bat @@ -0,0 +1,89 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/android/settings.gradle b/android/settings.gradle new file mode 100644 index 000000000..ab8f7824a --- /dev/null +++ b/android/settings.gradle @@ -0,0 +1,10 @@ +/* + * This file was generated by the Gradle 'init' task. + * + * The settings file is used to specify which projects to include in your build. + * + * Detailed information about configuring a multi-project build in Gradle can be found + * in the user manual at https://docs.gradle.org/6.6/userguide/multi_project_builds.html + */ + +rootProject.name = 'msgraph-sdk-java-core' diff --git a/build.gradle b/build.gradle index b6299c85c..923ea3704 100644 --- a/build.gradle +++ b/build.gradle @@ -13,12 +13,54 @@ plugins { id 'eclipse' id 'maven-publish' id 'signing' + id 'jacoco' + id 'com.github.spotbugs' version '4.6.0' } java { modularity.inferModulePath = true } +test { + useJUnitPlatform() + finalizedBy jacocoTestReport // report is always generated after tests run +} +jacocoTestReport { + dependsOn test // tests are required to run before generating the report +} + +jacoco { + toolVersion = "0.8.7-SNAPSHOT" //https://github.com/gradle/gradle/issues/15038 +} + +spotbugsMain { + excludeFilter = file("spotBugsExcludeFilter.xml") + reports { + html { + enabled = true + destination = file("$buildDir/reports/spotbugs/main/spotbugs.html") + stylesheet = 'fancy-hist.xsl' + } + } +} + +spotbugsTest { + excludeFilter = file("spotBugsExcludeFilter.xml") + reports { + html { + enabled = true + destination = file("$buildDir/reports/spotbugs/test/spotbugs.html") + stylesheet = 'fancy-hist.xsl' + } + } +} + +jacocoTestReport { + reports { + xml.enabled true + } +} + sourceSets { main { java { @@ -31,16 +73,10 @@ sourceSets { repositories { // You can declare any Maven/Ivy/file repository here. mavenCentral() + maven { url 'https://oss.sonatype.org/content/repositories/snapshots' } // for jacoco, until 0.8.7 gets released } -dependencies { - // Use JUnit test framework - testImplementation 'junit:junit:4.13.2' - - api 'com.squareup.okhttp3:okhttp:3.12.1' - - implementation 'com.google.code.gson:gson:2.8.6' -} +apply from: "gradle/dependencies.gradle" def pomConfig = { licenses { @@ -167,8 +203,8 @@ def fixAscNames = { name -> } compileJava { - sourceCompatibility = 1.7 - targetCompatibility = 1.7 + sourceCompatibility = 1.8 + targetCompatibility = 1.8 } def getVersionCode() { diff --git a/gradle.properties b/gradle.properties index 71c7cad16..817c47716 100644 --- a/gradle.properties +++ b/gradle.properties @@ -23,9 +23,9 @@ org.gradle.jvmargs=-XX:MaxPermSize=512m -Xmx2g mavenGroupId = com.microsoft.graph mavenArtifactId = microsoft-graph-core -mavenMajorVersion = 1 +mavenMajorVersion = 2 mavenMinorVersion = 0 -mavenPatchVersion = 9 +mavenPatchVersion = 0 mavenArtifactSuffix = #These values are used to run functional tests diff --git a/gradle/dependencies.gradle b/gradle/dependencies.gradle new file mode 100644 index 000000000..fc0167fec --- /dev/null +++ b/gradle/dependencies.gradle @@ -0,0 +1,13 @@ +dependencies { + // Use JUnit test framework + testImplementation 'org.junit.jupiter:junit-jupiter-api:5.7.1' + testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.7.1' + testImplementation 'org.mockito:mockito-inline:3.7.7' + + api 'com.squareup.okhttp3:okhttp:4.9.1' + + implementation 'com.google.guava:guava:30.1-jre' + + implementation 'com.google.code.gson:gson:2.8.6' + api 'com.azure:azure-identity:1.2.3' +} \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 6c9a22477..28ff446a2 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-6.6-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-6.8.1-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew old mode 100644 new mode 100755 diff --git a/pom.xml b/pom.xml index dc77c1858..235483938 100644 --- a/pom.xml +++ b/pom.xml @@ -8,7 +8,7 @@ com.microsoft.graph microsoft-graph-core - 1.0.6 + 2.0.0 pom @@ -17,10 +17,20 @@ gson 2.8.6 + + com.google.guava + guava + 30.1-jre + com.squareup.okhttp3 gson - 3.12.1 + 4.9.1 + + + com.azure + azure-identity + 1.2.0 \ No newline at end of file diff --git a/readme.md b/readme.md index f0f3bef71..bf926ff21 100644 --- a/readme.md +++ b/readme.md @@ -20,7 +20,7 @@ repositories { dependencies { // Include the sdk as a dependency - implementation 'com.microsoft.graph:microsoft-graph-core:1.0.9' + implementation 'com.microsoft.graph:microsoft-graph-core:2.0.0' } ``` @@ -32,7 +32,7 @@ Add the dependency in `dependencies` in pom.xml com.microsoft.graph microsoft-graph-core - 1.0.9 + 2.0.0 ``` @@ -48,20 +48,17 @@ Register your application by following the steps at [Register your app with the ### 2.2 Create an IAuthenticationProvider object -An instance of the **HttpClients** class handles building client. To create a new instance of this class, you need to provide an instance of `ICoreAuthenticationProvider`, which can authenticate requests to Microsoft Graph. +An instance of the **HttpClients** class handles building client. To create a new instance of this class, you need to provide an instance of `IAuthenticationProvider`, which can authenticate requests to Microsoft Graph. -### To get instance of HttpClients - -Auth in Java app [here](https://github.com/microsoftgraph/msgraph-sdk-java-auth) - -Auth in Android app [here](https://github.com/microsoftgraph/msgraph-sdk-android-auth) +For an example of how to get an authentication provider, see [choose a Microsoft Graph authentication provider](https://docs.microsoft.com/graph/sdks/choose-authentication-providers?tabs=Java). ### 2.3 Get a HttpClients object + You must get a **HttpClients** object to make requests against the service. ```java -OkHttpClient client = HttpClients.createDefault(iCoreAuthenticationProvider); +OkHttpClient client = HttpClients.createDefault(iAuthenticationProvider); ``` ## 3. Make requests against the service @@ -120,7 +117,9 @@ The Microsoft Graph SDK is open for contribution. To contribute to this project, ## 6. Supported Java versions -The Microsoft Graph SDK for Java library is supported at runtime for Java 7+ and [Android API revision 15](http://source.android.com/source/build-numbers.html) and greater. +The Microsoft Graph SDK for Java library is supported at runtime for Java 8 and [Android API revision 26](http://source.android.com/source/build-numbers.html) or greater. + +Android developers targeting lower android API levels can do so by [enabling desugaring](https://developer.android.com/studio/write/java8-support#library-desugaring) in their project. ## 7. License diff --git a/samples/deviceCodeSample/.gitignore b/samples/deviceCodeSample/.gitignore new file mode 100644 index 000000000..e34a1d606 --- /dev/null +++ b/samples/deviceCodeSample/.gitignore @@ -0,0 +1,34 @@ +# Compiled class file +*.class + +# Log file +*.log + +# BlueJ files +*.ctxt + +# Mobile Tools for Java (J2ME) +.mtj.tmp/ + +# Package Files # +*.jar +*.war +*.ear +*.zip +*.tar.gz +*.rar + +# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml +hs_err_pid* +/.gradle/ +/build/ +/bin/ + +#Eclipse +.project +.classpath +.settings + +# Maven +/target/ +local.properties \ No newline at end of file diff --git a/samples/deviceCodeSample/build.gradle b/samples/deviceCodeSample/build.gradle new file mode 100644 index 000000000..ea5dbaf67 --- /dev/null +++ b/samples/deviceCodeSample/build.gradle @@ -0,0 +1,16 @@ +plugins { + id 'java' +} + +group 'org.example' +version '1.0-SNAPSHOT' + +repositories { + mavenCentral() +} + +dependencies { + testCompile group: 'junit', name: 'junit', version: '4.12' + implementation project(':coreLibrary') + implementation 'com.azure:azure-identity:1.2.0' +} diff --git a/samples/deviceCodeSample/gradle/wrapper/gradle-wrapper.jar b/samples/deviceCodeSample/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 000000000..e708b1c02 Binary files /dev/null and b/samples/deviceCodeSample/gradle/wrapper/gradle-wrapper.jar differ diff --git a/samples/deviceCodeSample/gradle/wrapper/gradle-wrapper.properties b/samples/deviceCodeSample/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 000000000..be52383ef --- /dev/null +++ b/samples/deviceCodeSample/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-6.7-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/samples/deviceCodeSample/gradlew b/samples/deviceCodeSample/gradlew new file mode 100644 index 000000000..4f906e0c8 --- /dev/null +++ b/samples/deviceCodeSample/gradlew @@ -0,0 +1,185 @@ +#!/usr/bin/env sh + +# +# Copyright 2015 the original author or authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin or MSYS, switch paths to Windows format before running java +if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=`expr $i + 1` + done + case $i in + 0) set -- ;; + 1) set -- "$args0" ;; + 2) set -- "$args0" "$args1" ;; + 3) set -- "$args0" "$args1" "$args2" ;; + 4) set -- "$args0" "$args1" "$args2" "$args3" ;; + 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=`save "$@"` + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +exec "$JAVACMD" "$@" diff --git a/samples/deviceCodeSample/gradlew.bat b/samples/deviceCodeSample/gradlew.bat new file mode 100644 index 000000000..107acd32c --- /dev/null +++ b/samples/deviceCodeSample/gradlew.bat @@ -0,0 +1,89 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/samples/deviceCodeSample/settings.gradle b/samples/deviceCodeSample/settings.gradle new file mode 100644 index 000000000..79a5cd38a --- /dev/null +++ b/samples/deviceCodeSample/settings.gradle @@ -0,0 +1,4 @@ +include 'coreLibrary' + +project(':coreLibrary').projectDir = new File("../../") + diff --git a/samples/deviceCodeSample/src/main/java/DeviceCodeFlowMain.java b/samples/deviceCodeSample/src/main/java/DeviceCodeFlowMain.java new file mode 100644 index 000000000..b39fb3ad7 --- /dev/null +++ b/samples/deviceCodeSample/src/main/java/DeviceCodeFlowMain.java @@ -0,0 +1,45 @@ +import com.azure.identity.DeviceCodeCredential; +import com.azure.identity.DeviceCodeCredentialBuilder; +import com.microsoft.graph.authentication.TokenCredentialAuthProvider; +import com.microsoft.graph.httpcore.HttpClients; +import okhttp3.*; + +import java.io.IOException; +import java.util.Arrays; +import java.util.List; + + +public class DeviceCodeFlowMain { + + //Replace CLIENT_ID with your own client id from an app that is configured according to the requirements below + //for requirements visit: + //https://github.com/Azure/azure-sdk-for-java/wiki/Set-up-Your-Environment-for-Authentication#enable-applications-for-device-code-flow + private final static String CLIENT_ID = ""; + + //Set the scopes for your ms-graph request + private final static List SCOPES = Arrays.asList("User.ReadBasic.All", "User.Read"); + + public static void main(String[] args) throws Exception { + final DeviceCodeCredential deviceCodeCred = new DeviceCodeCredentialBuilder() + .clientId(CLIENT_ID) + .challengeConsumer(challenge -> System.out.println(challenge.getMessage())) + .build(); + + final TokenCredentialAuthProvider tokenCredAuthProvider = new TokenCredentialAuthProvider(SCOPES, deviceCodeCred); + final OkHttpClient httpClient = HttpClients.createDefault(tokenCredAuthProvider); + + final Request request = new Request.Builder().url("https://graph.microsoft.com/v1.0/me/").build(); + + httpClient.newCall(request).enqueue(new Callback() { + @Override + public void onResponse(Call call, Response response) throws IOException { + System.out.println(response.body().string()); + } + + @Override + public void onFailure(Call call, IOException e) { + e.printStackTrace(); + } + }); + } +} diff --git a/samples/interactiveBrowserSample/.gitignore b/samples/interactiveBrowserSample/.gitignore new file mode 100644 index 000000000..e34a1d606 --- /dev/null +++ b/samples/interactiveBrowserSample/.gitignore @@ -0,0 +1,34 @@ +# Compiled class file +*.class + +# Log file +*.log + +# BlueJ files +*.ctxt + +# Mobile Tools for Java (J2ME) +.mtj.tmp/ + +# Package Files # +*.jar +*.war +*.ear +*.zip +*.tar.gz +*.rar + +# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml +hs_err_pid* +/.gradle/ +/build/ +/bin/ + +#Eclipse +.project +.classpath +.settings + +# Maven +/target/ +local.properties \ No newline at end of file diff --git a/samples/interactiveBrowserSample/build.gradle b/samples/interactiveBrowserSample/build.gradle new file mode 100644 index 000000000..ea5dbaf67 --- /dev/null +++ b/samples/interactiveBrowserSample/build.gradle @@ -0,0 +1,16 @@ +plugins { + id 'java' +} + +group 'org.example' +version '1.0-SNAPSHOT' + +repositories { + mavenCentral() +} + +dependencies { + testCompile group: 'junit', name: 'junit', version: '4.12' + implementation project(':coreLibrary') + implementation 'com.azure:azure-identity:1.2.0' +} diff --git a/samples/interactiveBrowserSample/gradle/wrapper/gradle-wrapper.jar b/samples/interactiveBrowserSample/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 000000000..e708b1c02 Binary files /dev/null and b/samples/interactiveBrowserSample/gradle/wrapper/gradle-wrapper.jar differ diff --git a/samples/interactiveBrowserSample/gradle/wrapper/gradle-wrapper.properties b/samples/interactiveBrowserSample/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 000000000..be52383ef --- /dev/null +++ b/samples/interactiveBrowserSample/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-6.7-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/samples/interactiveBrowserSample/gradlew b/samples/interactiveBrowserSample/gradlew new file mode 100644 index 000000000..4f906e0c8 --- /dev/null +++ b/samples/interactiveBrowserSample/gradlew @@ -0,0 +1,185 @@ +#!/usr/bin/env sh + +# +# Copyright 2015 the original author or authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin or MSYS, switch paths to Windows format before running java +if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=`expr $i + 1` + done + case $i in + 0) set -- ;; + 1) set -- "$args0" ;; + 2) set -- "$args0" "$args1" ;; + 3) set -- "$args0" "$args1" "$args2" ;; + 4) set -- "$args0" "$args1" "$args2" "$args3" ;; + 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=`save "$@"` + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +exec "$JAVACMD" "$@" diff --git a/samples/interactiveBrowserSample/gradlew.bat b/samples/interactiveBrowserSample/gradlew.bat new file mode 100644 index 000000000..107acd32c --- /dev/null +++ b/samples/interactiveBrowserSample/gradlew.bat @@ -0,0 +1,89 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/samples/interactiveBrowserSample/settings.gradle b/samples/interactiveBrowserSample/settings.gradle new file mode 100644 index 000000000..b6260dfbe --- /dev/null +++ b/samples/interactiveBrowserSample/settings.gradle @@ -0,0 +1,5 @@ +include 'coreLibrary' + +project(':coreLibrary').projectDir = new File("../../") + + diff --git a/samples/interactiveBrowserSample/src/main/java/InteractiveBrowserMain.java b/samples/interactiveBrowserSample/src/main/java/InteractiveBrowserMain.java new file mode 100644 index 000000000..f9eb5a767 --- /dev/null +++ b/samples/interactiveBrowserSample/src/main/java/InteractiveBrowserMain.java @@ -0,0 +1,43 @@ +import java.io.IOException; +import java.util.Arrays; +import java.util.List; +import com.azure.identity.InteractiveBrowserCredential; +import com.azure.identity.InteractiveBrowserCredentialBuilder; +import com.microsoft.graph.authentication.*; +import okhttp3.*; +import com.microsoft.graph.httpcore.HttpClients; + +public class InteractiveBrowserMain { + + //Replace CLIENT_ID with your own client id from an adequately configured app + //for requirements visit: + //https://github.com/Azure/azure-sdk-for-java/wiki/Set-up-Your-Environment-for-Authentication#enable-applications-for-interactive-browser-oauth-2-flow + private final static String CLIENT_ID = ""; + + //Set the scopes for your ms-graph request + private final static List SCOPES = Arrays.asList("User.ReadBasic.All"); + + public static void main(String args[]) throws Exception { + final InteractiveBrowserCredential interactiveBrowserCredential = new InteractiveBrowserCredentialBuilder() + .clientId(CLIENT_ID) + .redirectUrl("http://localhost:8765") + .build(); + + final TokenCredentialAuthProvider tokenCredentialAuthProvider = new TokenCredentialAuthProvider(SCOPES, interactiveBrowserCredential); + final OkHttpClient httpClient = HttpClients.createDefault(tokenCredentialAuthProvider); + + final Request request = new Request.Builder().url("https://graph.microsoft.com/v1.0/me/").build(); + + httpClient.newCall(request).enqueue(new Callback() { + @Override + public void onResponse(Call call, Response response) throws IOException { + System.out.println(response.body().string()); + } + + @Override + public void onFailure(Call call, IOException e) { + e.printStackTrace(); + } + }); + } +} diff --git a/Scripts/getLatestVersion.ps1 b/scripts/getLatestVersion.ps1 similarity index 91% rename from Scripts/getLatestVersion.ps1 rename to scripts/getLatestVersion.ps1 index 4e935336b..d269bcaa8 100644 --- a/Scripts/getLatestVersion.ps1 +++ b/scripts/getLatestVersion.ps1 @@ -6,9 +6,11 @@ Retrieve the latest version of the library .Description Retrieves the latest version specified in the Gradle.Properties file - Uses the retrieved values to update the enviornment variable VERSION_STRING + Uses the retrieved values to update the environment variable VERSION_STRING .Parameter propertiesPath + The path pointing to the gradle.properties file. #> + Param( [string]$propertiesPath ) diff --git a/Scripts/incrementMinorVersion.ps1 b/scripts/incrementMinorVersion.ps1 similarity index 100% rename from Scripts/incrementMinorVersion.ps1 rename to scripts/incrementMinorVersion.ps1 diff --git a/spotBugsExcludeFilter.xml b/spotBugsExcludeFilter.xml new file mode 100644 index 000000000..c6b2fff2e --- /dev/null +++ b/spotBugsExcludeFilter.xml @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/main/java/com/microsoft/graph/authentication/BaseAuthenticationProvider.java b/src/main/java/com/microsoft/graph/authentication/BaseAuthenticationProvider.java new file mode 100644 index 000000000..4b73a2177 --- /dev/null +++ b/src/main/java/com/microsoft/graph/authentication/BaseAuthenticationProvider.java @@ -0,0 +1,27 @@ +package com.microsoft.graph.authentication; + +import java.net.URL; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Locale; + +import javax.annotation.Nonnull; + +/** + * Provides basic common methods for all authentication providers + */ +public abstract class BaseAuthenticationProvider implements IAuthenticationProvider { + private static final HashSet validGraphHostNames = new HashSet<>(Arrays.asList("graph.microsoft.com", "graph.microsoft.us", "dod-graph.microsoft.us", "graph.microsoft.de", "microsoftgraph.chinacloudapi.cn")); + /** + * Determines whether a request should be authenticated or not based on it's url. + * If you're implementing a custom provider, call that method first before getting the token + * @param requestUrl request URL that is about to be executed + * @return whether a token should be attached to this request + */ + protected boolean shouldAuthenticateRequestWithUrl(@Nonnull final URL requestUrl) { + if (requestUrl == null || !requestUrl.getProtocol().toLowerCase(Locale.ROOT).equals("https")) + return false; + final String hostName = requestUrl.getHost().toLowerCase(Locale.ROOT); + return validGraphHostNames.contains(hostName); + } +} diff --git a/src/main/java/com/microsoft/graph/authentication/IAuthenticationProvider.java b/src/main/java/com/microsoft/graph/authentication/IAuthenticationProvider.java new file mode 100644 index 000000000..ae1f478f4 --- /dev/null +++ b/src/main/java/com/microsoft/graph/authentication/IAuthenticationProvider.java @@ -0,0 +1,20 @@ +package com.microsoft.graph.authentication; + +import java.net.URL; +import java.util.concurrent.CompletableFuture; + +import javax.annotation.Nonnull; + +/** + * Authenticates requests to be sent to the API + */ +public interface IAuthenticationProvider { + /** + * Authenticates the request + * + * @param requestUrl the outgoing request URL + * @return a future with the token + */ + @Nonnull + CompletableFuture getAuthorizationTokenAsync(@Nonnull final URL requestUrl); +} diff --git a/src/main/java/com/microsoft/graph/authentication/TokenCredentialAuthProvider.java b/src/main/java/com/microsoft/graph/authentication/TokenCredentialAuthProvider.java new file mode 100644 index 000000000..56e28a443 --- /dev/null +++ b/src/main/java/com/microsoft/graph/authentication/TokenCredentialAuthProvider.java @@ -0,0 +1,63 @@ +package com.microsoft.graph.authentication; + +import com.azure.core.credential.TokenCredential; +import com.azure.core.credential.TokenRequestContext; + +import java.net.URL; +import java.util.Arrays; +import java.util.List; +import java.util.Objects; +import java.util.concurrent.CompletableFuture; + +import javax.annotation.Nonnull; + +/** + * An implementation of the Authentication Provider with Azure-identity + */ +public class TokenCredentialAuthProvider extends BaseAuthenticationProvider { + /** TokenCredential expected from user */ + private final TokenCredential tokenCredential; + /** Context options which can be optionally set by the user */ + private final TokenRequestContext context; + /** Default scope to use when no scopes are provided */ + private static final String DEFAULT_GRAPH_SCOPE = "https://graph.microsoft.com/.default"; + /** + * Creates an Authentication provider using a passed in TokenCredential + * + * @param tokenCredential Credential object inheriting the TokenCredential interface used to instantiate the Auth Provider + */ + public TokenCredentialAuthProvider(@Nonnull final TokenCredential tokenCredential) { + this(Arrays.asList(DEFAULT_GRAPH_SCOPE), tokenCredential); + } + + /** + * Creates an Authentication provider using a TokenCredential and list of scopes + * + * @param tokenCredential Credential object inheriting the TokenCredential interface used to instantiate the Auth Provider + * @param scopes Specified desired scopes of the Auth Provider + */ + public TokenCredentialAuthProvider(@Nonnull final List scopes, @Nonnull final TokenCredential tokenCredential) { + if(scopes == null || scopes.isEmpty()) { + throw new IllegalArgumentException("scopes parameter cannot be null or empty"); + } + this.context = new TokenRequestContext(); + this.context.setScopes(scopes); + this.tokenCredential = Objects.requireNonNull(tokenCredential, "tokenCredential parameter cannot be null."); + } + + /** + * Returns an AccessToken as a string + * + * @return String representing the retrieved AccessToken + */ + @Nonnull + public CompletableFuture getAuthorizationTokenAsync(@Nonnull final URL requestUrl) { + if(shouldAuthenticateRequestWithUrl(Objects.requireNonNull(requestUrl, "requestUrl parameter cannot be null"))) + return this.tokenCredential + .getToken(this.context) + .toFuture() + .thenApply(resp -> resp.getToken()); + else + return CompletableFuture.completedFuture((String)null); + } +} diff --git a/src/main/java/com/microsoft/graph/content/BatchRequest.java b/src/main/java/com/microsoft/graph/content/BatchRequest.java new file mode 100644 index 000000000..ae7b732c4 --- /dev/null +++ b/src/main/java/com/microsoft/graph/content/BatchRequest.java @@ -0,0 +1,85 @@ +// ------------------------------------------------------------------------------ +// Copyright (c) 2021 Microsoft Corporation +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sub-license, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// ------------------------------------------------------------------------------ + +package com.microsoft.graph.content; + +import java.util.List; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +import com.microsoft.graph.core.ClientException; +import com.microsoft.graph.core.IBaseClient; +import com.microsoft.graph.http.BaseRequest; +import com.microsoft.graph.http.HttpMethod; +import com.microsoft.graph.options.Option; + +/** Request for batch requests */ +public class BatchRequest extends BaseRequest { + + /** + * Instantiates a new batch request + * + * @param requestUrl the URL to send the request to + * @param client the client to use to execute the request + * @param options the options to apply to the request + */ + public BatchRequest(@Nonnull final String requestUrl, @Nonnull final IBaseClient client, @Nonnull final List options) { + super(requestUrl, client, options, BatchResponseContent.class); + } + + /** + * Send this request + * + * @return the response object + * @param content content of the batch request to execute + * @throws ClientException an exception occurs if there was an error while the request was sent + */ + @Nullable + public BatchResponseContent post(@Nullable final BatchRequestContent content) throws ClientException { + this.setHttpMethod(HttpMethod.POST); + final BatchResponseContent response = this.getClient().getHttpProvider().send(this, BatchResponseContent.class, content); + setSerializerOnSteps(response); + return response; + } + /** + * Send this request + * + * @return the response object + * @param content content of the batch request to execute + * @throws ClientException an exception occurs if there was an error while the request was sent + */ + @Nullable + public java.util.concurrent.CompletableFuture postAsync(@Nullable final BatchRequestContent content) throws ClientException { + this.setHttpMethod(HttpMethod.POST); + return this.getClient().getHttpProvider().sendAsync(this, BatchResponseContent.class, content).thenApply(response -> { + setSerializerOnSteps(response); + return response; + }); + } + private void setSerializerOnSteps(final BatchResponseContent response) { + if(response.responses != null) + for(final BatchResponseStep step : response.responses) { + step.serializer = this.getClient().getSerializer(); + } + } +} diff --git a/src/main/java/com/microsoft/graph/content/BatchRequestBuilder.java b/src/main/java/com/microsoft/graph/content/BatchRequestBuilder.java new file mode 100644 index 000000000..c6ceda75d --- /dev/null +++ b/src/main/java/com/microsoft/graph/content/BatchRequestBuilder.java @@ -0,0 +1,66 @@ +// ------------------------------------------------------------------------------ +// Copyright (c) 2021 Microsoft Corporation +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sub-license, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// ------------------------------------------------------------------------------ + +package com.microsoft.graph.content; + +import java.util.List; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +import com.microsoft.graph.core.IBaseClient; +import com.microsoft.graph.http.BaseRequestBuilder; +import com.microsoft.graph.options.Option; + +/** Request builder for batch requests */ +public class BatchRequestBuilder extends BaseRequestBuilder { + /** + * Instantiates a new batch request builder + * @param requestUrl the URL for the request + * @param client the client to use to execute the request + * @param options the request options + */ + public BatchRequestBuilder(@Nonnull final String requestUrl, @Nonnull final IBaseClient client, @Nonnull final List options) { + super(requestUrl, client, options); + } + /** + * Creates the request + * + * @param requestOptions the options for this request + * @return the IUserRequest instance + */ + @Nonnull + public BatchRequest buildRequest(@Nullable final com.microsoft.graph.options.Option... requestOptions) { + return buildRequest(getOptions(requestOptions)); + } + + /** + * Creates the request + * + * @param requestOptions the options for this request + * @return the IUserRequest instance + */ + @Nonnull + public BatchRequest buildRequest(@Nonnull final java.util.List requestOptions) { + return new BatchRequest(this.getRequestUrl(), getClient(), requestOptions); + } +} diff --git a/src/main/java/com/microsoft/graph/content/BatchRequestContent.java b/src/main/java/com/microsoft/graph/content/BatchRequestContent.java new file mode 100644 index 000000000..935f24c1c --- /dev/null +++ b/src/main/java/com/microsoft/graph/content/BatchRequestContent.java @@ -0,0 +1,184 @@ +// ------------------------------------------------------------------------------ +// Copyright (c) 2021 Microsoft Corporation +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sub-license, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// ------------------------------------------------------------------------------ + +package com.microsoft.graph.content; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Locale; +import java.util.Objects; +import java.util.concurrent.ThreadLocalRandom; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +import com.google.gson.annotations.Expose; +import com.google.gson.annotations.SerializedName; +import com.microsoft.graph.http.HttpMethod; +import com.microsoft.graph.http.IHttpRequest; +import com.microsoft.graph.options.HeaderOption; +import com.microsoft.graph.serializer.IJsonBackedObject; + +/** Respresents the content of a JSON batch request */ +public class BatchRequestContent { + /** Steps part of the batch request */ + @Expose + @Nullable + @SerializedName("requests") + public List> requests; + + /** + * Adds a request as a step to the current batch. Defaults to GET if the HTTP method is not set in the request. + * @param request the request to add as a step + * @return the id of the step that was just added to the batch + */ + @Nonnull + public String addBatchRequestStep(@Nonnull final IHttpRequest request) { + Objects.requireNonNull(request, "request parameter cannot be null"); + return addBatchRequestStep(request, request.getHttpMethod() == null ? HttpMethod.GET : request.getHttpMethod()); + } + /** + * Adds a request as a step to the current batch + * @param request the request to add as a step + * @param httpMethod the HttpMethod to execute the request with + * @return the id of the step that was just added to the batch + */ + @Nonnull + public String addBatchRequestStep(@Nonnull final IHttpRequest request, @Nonnull final HttpMethod httpMethod) { + return addBatchRequestStep(request, httpMethod, null); + } + /** + * Adds a request as a step to the current batch + * @param request the request to add as a step + * @param httpMethod the HttpMethod to execute the request with + * @param the type of the request body + * @param serializableBody the body of the request to be serialized + * @return the id of the step that was just added to the batch + */ + @Nonnull + public String addBatchRequestStep(@Nonnull final IHttpRequest request, @Nonnull final HttpMethod httpMethod, @Nullable final T serializableBody) { + return addBatchRequestStep(request, httpMethod, serializableBody, (String[])null); + } + /** + * Adds a request as a step to the current batch + * @param request the request to add as a step + * @param httpMethod the HttpMethod to execute the request with + * @param the type of the request body + * @param serializableBody the body of the request to be serialized + * @param dependsOnRequestsIds the ids of steps this request depends on + * @return the id of the step that was just added to the batch + */ + @Nonnull + public String addBatchRequestStep(@Nonnull final IHttpRequest request, @Nonnull final HttpMethod httpMethod, @Nullable final T serializableBody, @Nullable final String ...dependsOnRequestsIds) { + Objects.requireNonNull(request, "request parameter cannot be null"); + Objects.requireNonNull(httpMethod, "httpMethod parameter cannot be null"); + if(dependsOnRequestsIds != null) + for(final String id : dependsOnRequestsIds) { + if(getStepById(id) == null) + throw new IllegalArgumentException("the current request depends on a inexisting request"); + } + + if(requests == null) + requests = new ArrayList<>(); + + final Matcher protocolAndHostReplacementMatcher = protocolAndHostReplacementPattern.matcher(request.getRequestUrl().toString()); + final BatchRequestStep step = new BatchRequestStep() {{ + url = protocolAndHostReplacementMatcher.replaceAll(""); + body = serializableBody; + method = httpMethod.toString().toUpperCase(Locale.getDefault()); + dependsOn = dependsOnRequestsIds != null && dependsOnRequestsIds.length > 0 ? new HashSet(Arrays.asList(dependsOnRequestsIds)) : null; + id = getNextRequestId(); + }}; + + if(!request.getHeaders().isEmpty()) { + step.headers = new HashMap<>(); + for(final HeaderOption headerOption : request.getHeaders()) + step.headers.putIfAbsent(headerOption.getName().toLowerCase(Locale.getDefault()), headerOption.getValue().toString()); + } + if(step.body != null && step.body instanceof IJsonBackedObject && + (step.headers == null || !step.headers.containsKey(contentTypeHeaderKey))) { + if(step.headers == null) + step.headers = new HashMap<>(); + step.headers.putIfAbsent(contentTypeHeaderKey, "application/json"); + } + requests.add(step); + return step.id; + } + private static final String contentTypeHeaderKey = "content-type"; + /** + * Removes requests from the requests to be executed by the batch. Also removes any dependency references that might exist. + * @param stepIds ids of steps to be removed. + */ + public void removeBatchRequestStepWithId(@Nonnull final String ...stepIds) { + if(requests == null) return; + + for(final String stepId : stepIds) { + Objects.requireNonNull(stepId, "parameter stepIds cannot contain null values"); + requests.removeIf(x -> stepId.equals(x.id)); + for(final BatchRequestStep step : requests) { + if(step.dependsOn != null) { + step.dependsOn.removeIf(x -> stepId.equals(x)); + if(step.dependsOn.isEmpty()) + step.dependsOn = null; // so we don't send dependsOn: [] over the wire + } + } + } + } + /** + * Gets a step by its step id + * @param the type of the request body + * @param stepId the request step id returned from the add method + * @return the request corresponding to the provided id or null + */ + @Nullable + @SuppressWarnings("unchecked") + public BatchRequestStep getStepById(@Nonnull final String stepId) { + Objects.requireNonNull(stepId, "parameter stepId cannot be null"); + if(requests == null) return null; + + for(final BatchRequestStep step : requests) { + if(stepId.equals(step.id)) + return (BatchRequestStep)step; + } + return null; + } + /** pattern to replace the protocol and host part of the request if specified */ + @Nonnull + protected static final Pattern protocolAndHostReplacementPattern = Pattern.compile("(?i)^http[s]?:\\/\\/graph\\.microsoft\\.com\\/(?>v1\\.0|beta)\\/?"); // (?i) case insensitive + /** + * Generates a randomly available request id + * @return a random request id + */ + @Nonnull + protected String getNextRequestId() { + String requestId; + do { + requestId = Integer.toString(ThreadLocalRandom.current().nextInt(1, Integer.MAX_VALUE)); + } while(getStepById(requestId) != null); + return requestId; + } +} diff --git a/src/main/java/com/microsoft/graph/content/BatchRequestStep.java b/src/main/java/com/microsoft/graph/content/BatchRequestStep.java new file mode 100644 index 000000000..cb6ae415a --- /dev/null +++ b/src/main/java/com/microsoft/graph/content/BatchRequestStep.java @@ -0,0 +1,49 @@ +// ------------------------------------------------------------------------------ +// Copyright (c) 2021 Microsoft Corporation +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sub-license, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// ------------------------------------------------------------------------------ + +package com.microsoft.graph.content; + +import java.util.HashSet; + +import javax.annotation.Nullable; + +import com.google.gson.annotations.Expose; +import com.google.gson.annotations.SerializedName; + +/** the http request for the batch step */ +public class BatchRequestStep extends BatchStep { + /** The URL to query for the step */ + @Nullable + @Expose + @SerializedName("url") + public String url; + /** The HTTP method to use to execute the request */ + @Nullable + @Expose + @SerializedName("method") + public String method; + /** The IDs of the steps this step depends on before being executed */ + @Nullable + @Expose + @SerializedName("dependsOn") + public HashSet dependsOn; +} diff --git a/src/main/java/com/microsoft/graph/content/BatchResponseContent.java b/src/main/java/com/microsoft/graph/content/BatchResponseContent.java new file mode 100644 index 000000000..fe8022524 --- /dev/null +++ b/src/main/java/com/microsoft/graph/content/BatchResponseContent.java @@ -0,0 +1,59 @@ +// ------------------------------------------------------------------------------ +// Copyright (c) 2021 Microsoft Corporation +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sub-license, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// ------------------------------------------------------------------------------ + +package com.microsoft.graph.content; + +import java.util.List; +import java.util.Objects; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +import com.google.gson.JsonElement; +import com.google.gson.annotations.Expose; +import com.google.gson.annotations.SerializedName; + +/** Respresents the result of a JSON batch request */ +public class BatchResponseContent { + /** Responses to the steps from the request */ + @Nullable + @Expose + @SerializedName("responses") + public List> responses; + + /** + * Gets a response to a request in the batch by its id + * @param stepId Id of the request step in the batch request + * @return The step response corresponding to the ID or null + */ + @Nullable + public BatchResponseStep getResponseById(@Nonnull final String stepId) { + Objects.requireNonNull(stepId, "parameter stepId cannot be null"); + if(responses == null) return null; + + for(final BatchResponseStep step : responses) { + if(stepId.equals(step.id)) + return step; + } + return null; + } +} diff --git a/src/main/java/com/microsoft/graph/content/BatchResponseStep.java b/src/main/java/com/microsoft/graph/content/BatchResponseStep.java new file mode 100644 index 000000000..feb1d13a1 --- /dev/null +++ b/src/main/java/com/microsoft/graph/content/BatchResponseStep.java @@ -0,0 +1,67 @@ +// ------------------------------------------------------------------------------ +// Copyright (c) 2021 Microsoft Corporation +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sub-license, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// ------------------------------------------------------------------------------ +package com.microsoft.graph.content; + +import java.util.ArrayList; +import java.util.Objects; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +import com.google.gson.JsonElement; +import com.google.gson.annotations.Expose; +import com.google.gson.annotations.SerializedName; +import com.microsoft.graph.http.GraphErrorResponse; +import com.microsoft.graph.http.GraphFatalServiceException; +import com.microsoft.graph.http.GraphServiceException; +import com.microsoft.graph.serializer.ISerializer; + +/** Response for the batch step */ +public class BatchResponseStep extends BatchStep { + /** Http status code of the response */ + @Expose + @SerializedName("status") + public int status; + /** Serializer to use for response deserialization */ + @Nullable + protected ISerializer serializer; + + /** + * Returned the deserialized response body of the current step + * @param type of the response body + * @param resultClass class of the resulting response body + * @return the deserialized response body + * @throws GraphServiceException when a bad request was sent + * @throws GraphFatalServiceException when the service did not complete the operation as expected because of an internal error + */ + @Nullable + public T2 getDeserializedBody(@Nonnull final Class resultClass) throws GraphServiceException, GraphFatalServiceException { + Objects.requireNonNull(resultClass, "parameter resultClass cannot be null"); + if(serializer == null || body == null || !(body instanceof JsonElement)) return null; + + final GraphErrorResponse error = serializer.deserializeObject((JsonElement)body, GraphErrorResponse.class); + if(error == null || error.error == null) { + return serializer.deserializeObject((JsonElement)body, resultClass); + } else + throw GraphServiceException.createFromResponse("", "", new ArrayList<>(), "", headers, "", status, error, false); + } +} diff --git a/src/main/java/com/microsoft/graph/content/BatchStep.java b/src/main/java/com/microsoft/graph/content/BatchStep.java new file mode 100644 index 000000000..89773904e --- /dev/null +++ b/src/main/java/com/microsoft/graph/content/BatchStep.java @@ -0,0 +1,49 @@ +// ------------------------------------------------------------------------------ +// Copyright (c) 2021 Microsoft Corporation +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sub-license, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// ------------------------------------------------------------------------------ + +package com.microsoft.graph.content; + +import java.util.HashMap; + +import javax.annotation.Nullable; + +import com.google.gson.annotations.Expose; +import com.google.gson.annotations.SerializedName; + +/** Common abstractions between batch request steps and batch response steps */ +public abstract class BatchStep { + /** The Id of the step */ + @Expose + @Nullable + @SerializedName("id") + public String id; + /** The request/response headers for the step */ + @Expose + @Nullable + @SerializedName("headers") + public HashMap headers; + /** The body of request/response for the step */ + @Nullable + @Expose + @SerializedName("body") + public T body; +} diff --git a/src/main/java/com/microsoft/graph/content/MSBatchRequestContent.java b/src/main/java/com/microsoft/graph/content/MSBatchRequestContent.java deleted file mode 100644 index 4b9fc0a9e..000000000 --- a/src/main/java/com/microsoft/graph/content/MSBatchRequestContent.java +++ /dev/null @@ -1,185 +0,0 @@ -package com.microsoft.graph.content; - -import java.io.IOException; -import java.util.Arrays; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import java.util.concurrent.ThreadLocalRandom; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -import com.google.gson.JsonArray; -import com.google.gson.JsonElement; -import com.google.gson.JsonObject; -import com.google.gson.JsonParser; -import com.google.gson.JsonPrimitive; -import com.google.gson.JsonParseException; - -import okhttp3.Headers; -import okhttp3.Request; -import okhttp3.RequestBody; -import okio.Buffer; - -public class MSBatchRequestContent { - private final LinkedHashMap batchRequestStepsHashMap; - - // Maximum number of requests that can be sent in a batch - public static final int MAX_NUMBER_OF_REQUESTS = 20; - - /* - * Creates Batch request content using list provided - * - * @param batchRequestStepsArray List of batch steps for batching - */ - public MSBatchRequestContent(final List batchRequestStepsArray) { - if (batchRequestStepsArray.size() > MAX_NUMBER_OF_REQUESTS) - throw new IllegalArgumentException("Number of batch request steps cannot exceed " + MAX_NUMBER_OF_REQUESTS); - - this.batchRequestStepsHashMap = new LinkedHashMap<>(); - for (final MSBatchRequestStep requestStep : batchRequestStepsArray) - addBatchRequestStep(requestStep); - } - - /* - * Creates empty batch request content - */ - public MSBatchRequestContent() { - this.batchRequestStepsHashMap = new LinkedHashMap<>(); - } - - /* - * @param batchRequestStep Batch request step adding to batch content - * - * @return true or false based on addition or no addition of batch request step - * given - */ - public boolean addBatchRequestStep(final MSBatchRequestStep batchRequestStep) { - if (batchRequestStepsHashMap.containsKey(batchRequestStep.getRequestId()) || - batchRequestStepsHashMap.size() >= MAX_NUMBER_OF_REQUESTS) - return false; - batchRequestStepsHashMap.put(batchRequestStep.getRequestId(), batchRequestStep); - return true; - } - - /** - * Add steps to batch from OkHttp.Request - * @param request the request to add to the batch - * @param arrayOfDependsOnIds ids of steps this step depends on - * @return the step id - */ - public String addBatchRequestStep(final Request request, final String... arrayOfDependsOnIds) { - String requestId; - do { - requestId = Integer.toString(ThreadLocalRandom.current().nextInt(1, Integer.MAX_VALUE)); - } while(batchRequestStepsHashMap.keySet().contains(requestId)); - if(addBatchRequestStep(new MSBatchRequestStep(requestId, request, Arrays.asList(arrayOfDependsOnIds)))) - return requestId; - else - throw new IllegalArgumentException("unable to add step to batch. Number of batch request steps cannot exceed " + MAX_NUMBER_OF_REQUESTS); - } - - /* - * @param requestId Id of Batch request step to be removed - * - * @return true or false based on removal or no removal of batch request step - * with given id - */ - public boolean removeBatchRequestStepWithId(final String requestId) { - boolean removed = false; - if (batchRequestStepsHashMap.containsKey(requestId)) { - batchRequestStepsHashMap.remove(requestId); - removed = true; - for (final Map.Entry steps : batchRequestStepsHashMap.entrySet()) { - if (steps.getValue() != null && steps.getValue().getArrayOfDependsOnIds() != null) { - while (steps.getValue().getArrayOfDependsOnIds().remove(requestId)) - ; - } - } - } - return removed; - } - - /* - * @return Batch request content's json as String - */ - public String getBatchRequestContent() { - final JsonObject batchRequestContentMap = new JsonObject(); - final JsonArray batchContentArray = new JsonArray(); - for (final Map.Entry requestStep : batchRequestStepsHashMap.entrySet()) { - batchContentArray.add(getBatchRequestObjectFromRequestStep(requestStep.getValue())); - } - batchRequestContentMap.add("requests", batchContentArray); - - final String content = batchRequestContentMap.toString(); - return content; - } - - private static final Pattern protocolAndHostReplacementPattern = Pattern.compile("(?i)^http[s]?:\\/\\/graph\\.microsoft\\.com\\/(?>v1\\.0|beta)\\/?"); // (?i) case insensitive - private JsonObject getBatchRequestObjectFromRequestStep(final MSBatchRequestStep batchRequestStep) { - final JsonObject contentmap = new JsonObject(); - contentmap.add("id", new JsonPrimitive(batchRequestStep.getRequestId())); - - final Matcher protocolAndHostReplacementMatcher = protocolAndHostReplacementPattern.matcher(batchRequestStep.getRequest().url().toString()); - - final String url = protocolAndHostReplacementMatcher.replaceAll(""); - contentmap.add("url", new JsonPrimitive(url)); - - contentmap.add("method", new JsonPrimitive(batchRequestStep.getRequest().method().toString())); - - final Headers headers = batchRequestStep.getRequest().headers(); - if (headers != null && headers.size() != 0) { - final JsonObject headerMap = new JsonObject(); - for (final Map.Entry> entry : headers.toMultimap().entrySet()) { - headerMap.add(entry.getKey(), new JsonPrimitive(getHeaderValuesAsString(entry.getValue()))); - } - contentmap.add("headers", headerMap); - } - - final List arrayOfDependsOnIds = batchRequestStep.getArrayOfDependsOnIds(); - if (arrayOfDependsOnIds != null) { - final JsonArray array = new JsonArray(); - for (final String dependsOnId : arrayOfDependsOnIds) - array.add(dependsOnId); - contentmap.add("dependsOn", array); - } - - final RequestBody body = batchRequestStep.getRequest().body(); - if (body != null) { - try { - contentmap.add("body", requestBodyToJSONObject(batchRequestStep.getRequest())); - } catch (IOException | JsonParseException e) { - e.printStackTrace(); - } - } - return contentmap; - } - - private String getHeaderValuesAsString(final List list) { - if (list == null || list.size() == 0) - return ""; - final StringBuilder builder = new StringBuilder(list.get(0)); - for (int i = 1; i < list.size(); i++) { - builder.append(";"); - builder.append(list.get(i)); - } - return builder.toString(); - } - - private JsonObject requestBodyToJSONObject(final Request request) throws IOException, JsonParseException { - if (request == null || request.body() == null) - return null; - final Request copy = request.newBuilder().build(); - final Buffer buffer = new Buffer(); - copy.body().writeTo(buffer); - final String requestBody = buffer.readUtf8(); - if(requestBody == null || requestBody == "") - return null; - final JsonElement requestBodyElement = JsonParser.parseString(requestBody); - if(requestBodyElement == null || !requestBodyElement.isJsonObject()) - return null; - else - return requestBodyElement.getAsJsonObject(); - } - -} diff --git a/src/main/java/com/microsoft/graph/content/MSBatchRequestStep.java b/src/main/java/com/microsoft/graph/content/MSBatchRequestStep.java deleted file mode 100644 index 9e4265a41..000000000 --- a/src/main/java/com/microsoft/graph/content/MSBatchRequestStep.java +++ /dev/null @@ -1,36 +0,0 @@ -package com.microsoft.graph.content; - -import java.util.List; - -import okhttp3.Request; - -public class MSBatchRequestStep { - private String requestId; - private Request request; - private List arrayOfDependsOnIds; - - public MSBatchRequestStep(String requestId, Request request, List arrayOfDependsOnIds) { - if(requestId == null) - throw new IllegalArgumentException("Request Id cannot be null."); - if(requestId.length() == 0) - throw new IllegalArgumentException("Request Id cannot be empty."); - if(request == null) - throw new IllegalArgumentException("Request cannot be null."); - - this.requestId = requestId; - this.request = request; - this.arrayOfDependsOnIds = arrayOfDependsOnIds; - } - - public String getRequestId() { - return requestId; - } - - public Request getRequest() { - return request; - } - - public List getArrayOfDependsOnIds(){ - return arrayOfDependsOnIds; - } -} diff --git a/src/main/java/com/microsoft/graph/content/MSBatchResponseContent.java b/src/main/java/com/microsoft/graph/content/MSBatchResponseContent.java deleted file mode 100644 index ca40384e9..000000000 --- a/src/main/java/com/microsoft/graph/content/MSBatchResponseContent.java +++ /dev/null @@ -1,257 +0,0 @@ -package com.microsoft.graph.content; - -import java.io.IOException; -import java.util.LinkedHashMap; -import java.util.Iterator; -import java.util.Map; - -import com.google.gson.JsonArray; -import com.google.gson.JsonElement; -import com.google.gson.JsonObject; -import com.google.gson.JsonParser; -import com.google.gson.JsonParseException; - -import okhttp3.MediaType; -import okhttp3.Request; -import okhttp3.RequestBody; -import okhttp3.Response; -import okhttp3.ResponseBody; -import okio.Buffer; - -public class MSBatchResponseContent { - - private final Response batchResponse; - private LinkedHashMap batchRequestsHashMap; - private JsonArray batchResponseArray; - private String nextLink; - - /* - * @param batchResponse OkHttp batch response on execution of batch requests - */ - public MSBatchResponseContent(final Response batchResponse) { - this.batchResponse = batchResponse; - update(batchResponse); - } - - /* - * Returns OkHttp Response of given request Id - * - * @param requestId Request Id of batch step - * - * @return OkHttp Response corresponding to requestId - */ - public Response getResponseById(final String requestId) { - if (batchResponseArray == null) - return null; - - final JsonArray responses = batchResponseArray; - - for (final JsonElement response : responses) { - if(!response.isJsonObject()) - continue; - final JsonObject jsonresponse = response.getAsJsonObject(); - final JsonElement idElement = jsonresponse.get("id"); - if (idElement != null && idElement.isJsonPrimitive()) { - final String id = idElement.getAsString(); - if (id.compareTo(requestId) == 0) { - final Response.Builder builder = new Response.Builder(); - - // Put corresponding request into the constructed response - builder.request(batchRequestsHashMap.get(requestId)); - // copy protocol and message same as of batch response - builder.protocol(batchResponse.protocol()); - builder.message(batchResponse.message()); - - // Put status code of the corresponding request in JsonArray - final JsonElement statusElement = jsonresponse.get("status"); - if (statusElement != null && statusElement.isJsonPrimitive()) { - final Long status = statusElement.getAsLong(); - builder.code(status.intValue()); - } - - // Put body from response array for corresponding id into constructing response - final JsonElement jsonBodyElement = jsonresponse.get("body"); - if (jsonBodyElement != null && jsonBodyElement.isJsonObject()) { - final JsonObject JsonObject = jsonBodyElement.getAsJsonObject(); - final String bodyAsString = JsonObject.toString(); - final ResponseBody responseBody = ResponseBody - .create(MediaType.parse("application/json; charset=utf-8"), bodyAsString); - builder.body(responseBody); - } - - // Put headers from response array for corresponding id into constructing - // response - final JsonElement jsonheadersElement = jsonresponse.get("headers"); - if (jsonheadersElement != null && jsonheadersElement.isJsonObject()) { - final JsonObject jsonheaders = jsonheadersElement.getAsJsonObject(); - for (final String key : jsonheaders.keySet()) { - final JsonElement strValueElement = jsonheaders.get(key); - if (strValueElement != null && strValueElement.isJsonPrimitive()) { - final String strvalue = strValueElement.getAsString(); - for (final String value : strvalue.split(";")) { - builder.header(key, value); - } - } - } - } - return builder.build(); - } - } - } - return null; - } - - /** - * Get map of id and responses - * - * @return responses in Map of id and response - */ - public Map getResponses() { - if (batchResponseArray == null) - return null; - final Map responsesMap = new LinkedHashMap<>(); - for (final String id : batchRequestsHashMap.keySet()) { - responsesMap.put(id, getResponseById(id)); - } - return responsesMap; - } - - /** - * Get iterator over the responses - * - * @return iterator for responses - */ - public Iterator> getResponsesIterator() { - final Map responsesMap = getResponses(); - return responsesMap != null ? responsesMap.entrySet().iterator() : null; - } - - public void update(final Response batchResponse) { - if (batchResponse == null) - throw new IllegalArgumentException("Batch Response cannot be null"); - - final Map requestMap = createBatchRequestsHashMap(batchResponse); - if (batchRequestsHashMap == null) - batchRequestsHashMap = new LinkedHashMap<>(); - if (requestMap != null) - batchRequestsHashMap.putAll(requestMap); - - if (batchResponse.body() != null) { - try { - final String batchResponseData = batchResponse.body().string(); - if (batchResponseData != null) { - final JsonObject batchResponseObj = stringToJSONObject(batchResponseData); - if (batchResponseObj != null) { - - final JsonElement nextLinkElement = batchResponseObj.get("@odata.nextLink"); - if (nextLinkElement != null && nextLinkElement.isJsonPrimitive()) - nextLink = nextLinkElement.getAsString(); - - if (batchResponseArray == null) - batchResponseArray = new JsonArray(); - - final JsonElement responseArrayElement = batchResponseObj.get("responses"); - if (responseArrayElement != null && responseArrayElement.isJsonArray()) { - final JsonArray responseArray = responseArrayElement.getAsJsonArray(); - batchResponseArray.addAll(responseArray); - } - } - } - } catch (final IOException e) { - e.printStackTrace(); - } - } - } - - /* - * @return nextLink of batch response - */ - public String nextLink() { - return nextLink; - } - - private Map createBatchRequestsHashMap(final Response batchResponse) { - if (batchResponse == null) - return null; - try { - final Map batchRequestsHashMap = new LinkedHashMap<>(); - final JsonObject requestJSONObject = requestBodyToJSONObject(batchResponse.request()); - final JsonElement requestArrayElement = requestJSONObject.get("requests"); - if (requestArrayElement != null && requestArrayElement.isJsonArray()) { - final JsonArray requestArray = requestArrayElement.getAsJsonArray(); - for (final JsonElement item : requestArray) { - if(!item.isJsonObject()) - continue; - final JsonObject requestObject = item.getAsJsonObject(); - - final Request.Builder builder = new Request.Builder(); - - final JsonElement urlElement = requestObject.get("url"); - if (urlElement != null && urlElement.isJsonPrimitive()) { - final StringBuilder fullUrl = new StringBuilder( - batchResponse.request().url().toString().replace("$batch", "")); - fullUrl.append(urlElement.getAsString()); - builder.url(fullUrl.toString()); - } - final JsonElement jsonHeadersElement = requestObject.get("headers"); - if (jsonHeadersElement != null && jsonHeadersElement.isJsonObject()) { - final JsonObject jsonheaders = jsonHeadersElement.getAsJsonObject(); - for (final String key : jsonheaders.keySet()) { - final JsonElement strvalueElement = jsonheaders.get(key); - if (strvalueElement != null && strvalueElement.isJsonPrimitive()) { - final String strvalue = strvalueElement.getAsString(); - for (final String value : strvalue.split("; ")) { - builder.header(key, value); - } - } - } - } - final JsonElement jsonBodyElement = requestObject.get("body"); - final JsonElement jsonMethodElement = requestObject.get("method"); - if (jsonBodyElement != null && jsonMethodElement != null - && jsonBodyElement.isJsonObject() && jsonMethodElement.isJsonPrimitive()) { - final JsonObject JsonObject = jsonBodyElement.getAsJsonObject(); - final String bodyAsString = JsonObject.toString(); - final RequestBody requestBody = RequestBody - .create(MediaType.parse("application/json; charset=utf-8"), bodyAsString); - builder.method(jsonMethodElement.getAsString(), requestBody); - } else if (jsonMethodElement != null) { - builder.method(jsonMethodElement.getAsString(), null); - } - final JsonElement jsonIdElement = requestObject.get("id"); - if (jsonIdElement != null && jsonIdElement.isJsonPrimitive()) { - batchRequestsHashMap.put(jsonIdElement.getAsString(), builder.build()); - } - } - } - return batchRequestsHashMap; - - } catch (IOException | JsonParseException e) { - e.printStackTrace(); - } - return null; - } - - private JsonObject stringToJSONObject(final String input) { - JsonObject JsonObject = null; - try { - if (input != null) { - JsonObject = JsonParser.parseString(input).getAsJsonObject(); - } - } catch (final Exception e) { - e.printStackTrace(); - } - return JsonObject; - } - - private JsonObject requestBodyToJSONObject(final Request request) throws IOException, JsonParseException { - if (request == null || request.body() == null) - return null; - final Request copy = request.newBuilder().build(); - final Buffer buffer = new Buffer(); - copy.body().writeTo(buffer); - final String requestBody = buffer.readUtf8(); - final JsonObject JsonObject = JsonParser.parseString(requestBody).getAsJsonObject(); - return JsonObject; - } -} diff --git a/src/main/java/com/microsoft/graph/core/BaseClient.java b/src/main/java/com/microsoft/graph/core/BaseClient.java new file mode 100644 index 000000000..cc0a2e333 --- /dev/null +++ b/src/main/java/com/microsoft/graph/core/BaseClient.java @@ -0,0 +1,378 @@ +// ------------------------------------------------------------------------------ +// Copyright (c) 2017 Microsoft Corporation +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sub-license, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// ------------------------------------------------------------------------------ + +package com.microsoft.graph.core; + +import com.google.gson.JsonElement; +import com.microsoft.graph.http.CoreHttpProvider; +import com.microsoft.graph.http.IHttpProvider; +import com.microsoft.graph.httpcore.HttpClients; +import com.microsoft.graph.authentication.IAuthenticationProvider; +import com.microsoft.graph.content.BatchRequestBuilder; +import com.microsoft.graph.logger.DefaultLogger; +import com.microsoft.graph.logger.ILogger; +import com.microsoft.graph.serializer.DefaultSerializer; +import com.microsoft.graph.serializer.ISerializer; + +import javax.annotation.Nullable; + +import java.util.Collections; +import java.util.Objects; + +import javax.annotation.Nonnull; + +import okhttp3.OkHttpClient; +import okhttp3.Request; + +/** + * A client that communications with an OData service + * @param type of a request for the native http client + */ +public class BaseClient implements IBaseClient { + /** + * Restricted constructor + */ + protected BaseClient() { + } + + /** + * The default endpoint for the Microsoft Graph Service + */ + public static final String DEFAULT_GRAPH_ENDPOINT = "https://graph.microsoft.com/v1.0"; + + /** + * The current endpoint + */ + private String endpoint; + + @Override + @Nonnull + public String getServiceRoot() { + if (endpoint == null) { + endpoint = DEFAULT_GRAPH_ENDPOINT; + } + return endpoint; + } + + @Override + public void setServiceRoot(@Nonnull final String value) { + endpoint = Objects.requireNonNull(value, "value parameter cannot be null"); + } + + /** + * Send a custom request to Graph + * + * @param url + * the full URL to make a request with + * @param responseType + * the response class to deserialize the response into + * @return the instance of this builder + */ + @Nonnull + public CustomRequestBuilder customRequest(@Nonnull final String url, @Nonnull final Class responseType) { + Objects.requireNonNull(url, "url parameter cannot be null"); + Objects.requireNonNull(responseType, "responseType parameter cannot be null"); + return new CustomRequestBuilder<>(getServiceRoot() + url, this, null, responseType); + } + + /** + * Send a custom request to Graph + * + * @param url + * the full URL to make a request with + * @return the instance of this builder + */ + @Nonnull + public CustomRequestBuilder customRequest(@Nonnull final String url) { + return this.customRequest(url, JsonElement.class); + } + + /** + * Get the batch request builder. + * @return a request builder to execute a batch. + */ + @Nonnull + public BatchRequestBuilder batch() { + return new BatchRequestBuilder(getServiceRoot() + "/$batch", this, Collections.emptyList()); + } + + /** + * Gets the builder to start configuring the client + * + * @return builder to start configuring the client + */ + @Nonnull + public static Builder builder() { + return builder(OkHttpClient.class, Request.class); + } + + /** + * Gets the builder to start configuring the client + * + * @param the type of the native http client + * @param the type of the native http request + * @param nativeClientClass the class of the native http client + * @param nativeRequestClass the class of the native http request + * @return builder to start configuring the client + */ + @Nonnull + public static Builder builder(@Nonnull final Class nativeClientClass, @Nonnull final Class nativeRequestClass) { + return new Builder<>(); + } + + /** + * Builder to help configure the Graph service client + * @param type of the native http library client + * @param type of a request for the native http client + */ + public static class Builder { + private ISerializer serializer; + private IHttpProvider httpProvider; + private ILogger logger; + private httpClientType httpClient; + private IAuthenticationProvider auth; + + private IAuthenticationProvider getAuthenticationProvider() { + if(auth == null) { + throw new NullPointerException("auth"); + } else { + return auth; + } + } + private ILogger getLogger() { + if(logger == null) { + return new DefaultLogger(); + } else { + return logger; + } + } + private ISerializer getSerializer() { + if(serializer == null) { + return new DefaultSerializer(getLogger()); + } else { + return serializer; + } + } + @SuppressWarnings("unchecked") + private httpClientType getHttpClient() { + if(httpClient == null) { + return (httpClientType)HttpClients.createDefault(getAuthenticationProvider()); + } else { + return httpClient; + } + } + @SuppressWarnings("unchecked") + private IHttpProvider getHttpProvider() { + if(httpProvider == null) { + return (IHttpProvider)new CoreHttpProvider(getSerializer(), getLogger(), (OkHttpClient)getHttpClient()); + } else { + return httpProvider; + } + } + + /** + * Sets the serializer. + * + * @param serializer + * the serializer + * @return the instance of this builder + */ + @Nonnull + public Builder serializer(@Nonnull final ISerializer serializer) { + Objects.requireNonNull(serializer, "parameter serializer cannot be null"); + this.serializer = serializer; + return this; + } + + /** + * Sets the httpProvider + * + * @param httpProvider + * the httpProvider + * @return the instance of this builder + */ + @Nonnull + public Builder httpProvider(@Nonnull final IHttpProvider httpProvider) { + Objects.requireNonNull(httpProvider, "parameter httpProvider cannot be null"); + this.httpProvider = httpProvider; + return this; + } + + /** + * Sets the logger + * + * @param logger + * the logger + * @return the instance of this builder + */ + @Nonnull + public Builder logger(@Nonnull final ILogger logger) { + Objects.requireNonNull(logger, "parameter logger cannot be null"); + this.logger = logger; + return this; + } + + /** + * Sets the http client + * + * @param client the http client + * + * @return the instance of this builder + */ + @Nonnull + public Builder httpClient(@Nonnull final httpClientType client) { + Objects.requireNonNull(client, "parameter client cannot be null"); + this.httpClient = client; + return this; + } + + /** + * Sets the authentication provider + * + * @param auth the authentication provider + * @return the instance of this builder + */ + @Nonnull + public Builder authenticationProvider(@Nonnull final IAuthenticationProvider auth) { + Objects.requireNonNull(auth, "parameter auth cannot be null"); + this.auth = auth; + return this; + } + + /** + * Builds and returns the Graph service client. + * + * @param instance the instance to set the information for + * @param the type of the client to return + * @return the Graph service client object + * @throws ClientException + * if there was an exception creating the client + */ + @Nonnull + protected > ClientType buildClient(@Nonnull ClientType instance) throws ClientException { + Objects.requireNonNull(instance, "The instance cannot be null"); + instance.setHttpProvider(this.getHttpProvider()); + instance.setLogger(this.getLogger()); + instance.setSerializer(this.getSerializer()); + return instance; + } + + /** + * Builds and returns the Graph service client. + * + * @return the Graph service client object + * @throws ClientException + * if there was an exception creating the client + */ + @Nonnull + public IBaseClient buildClient() throws ClientException { + return buildClient(new BaseClient<>()); + } + } + + /** + * The HTTP provider instance + */ + private IHttpProvider httpProvider; + + /** + * The logger + */ + private ILogger logger; + + /** + * The serializer instance + */ + private ISerializer serializer; + + /** + * Gets the HTTP provider + * + * @return The HTTP provider + */ + @Override + @Nullable + public IHttpProvider getHttpProvider() { + return httpProvider; + } + + /** + * Gets the logger + * + * @return The logger + */ + @Nullable + public ILogger getLogger() { + return logger; + } + + /** + * Gets the serializer + * + * @return The serializer + */ + @Override + @Nullable + public ISerializer getSerializer() { + return serializer; + } + + /** + * Sets the logger + * + * @param logger The logger + */ + protected void setLogger(@Nonnull final ILogger logger) { + Objects.requireNonNull(logger, "parameter logger cannot be null"); + this.logger = logger; + } + + /** + * Sets the HTTP provider + * + * @param httpProvider The HTTP provider + */ + protected void setHttpProvider(@Nonnull final IHttpProvider httpProvider) { + Objects.requireNonNull(httpProvider, "parameter httpProvider cannot be null"); + this.httpProvider = httpProvider; + } + + /** + * Sets the serializer + * + * @param serializer The serializer + */ + public void setSerializer(@Nonnull final ISerializer serializer) { + Objects.requireNonNull(serializer, "parameter serializer cannot be null"); + this.serializer = serializer; + } + + /** + * Gets the service SDK version if the service SDK is in use, null otherwise + * @return the service SDK version if the service SDK is in use, null otherwise + */ + @Override + @Nullable + public String getServiceSDKVersion() { + return null; + } +} diff --git a/src/main/java/com/microsoft/graph/core/ClientException.java b/src/main/java/com/microsoft/graph/core/ClientException.java new file mode 100644 index 000000000..a5ab34469 --- /dev/null +++ b/src/main/java/com/microsoft/graph/core/ClientException.java @@ -0,0 +1,44 @@ +// ------------------------------------------------------------------------------ +// Copyright (c) 2017 Microsoft Corporation +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sub-license, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// ------------------------------------------------------------------------------ + +package com.microsoft.graph.core; + +import javax.annotation.Nullable; +import javax.annotation.Nonnull; + +/** + * An exception from the client. + */ +public class ClientException extends RuntimeException { + + private static final long serialVersionUID = -1066560879567392559L; + + /** + * Creates the client exception + * + * @param message the message to display + * @param ex the exception from + */ + public ClientException(@Nonnull final String message, @Nullable final Throwable ex) { + super(message, ex); + } +} diff --git a/src/main/java/com/microsoft/graph/core/CustomRequestBuilder.java b/src/main/java/com/microsoft/graph/core/CustomRequestBuilder.java new file mode 100644 index 000000000..5915eb32b --- /dev/null +++ b/src/main/java/com/microsoft/graph/core/CustomRequestBuilder.java @@ -0,0 +1,56 @@ +package com.microsoft.graph.core; + +import java.util.List; +import java.util.Objects; + +import javax.annotation.Nullable; +import javax.annotation.Nonnull; + +import com.microsoft.graph.http.BaseRequestBuilder; +import com.microsoft.graph.http.CustomRequest; +import com.microsoft.graph.options.Option; + +/** + * The class for the CustomRequestBuilder + * + * If the provided URL is malformed, the ClientException will contain a MalformedURLException + */ +public class CustomRequestBuilder extends BaseRequestBuilder { + /** Type to use for response deserialization */ + public final Class responseType; + + /** + * Instanciates a new custom request builder + * + * @param requestUrl the URL to send the request to + * @param client the client to use for the request + * @param requestOptions options to apply to the request + * @param responseType type to use for response deserialization + */ + public CustomRequestBuilder(@Nonnull final String requestUrl, @Nonnull final IBaseClient client, @Nullable final List requestOptions, @Nonnull final Class responseType) { + super(requestUrl, client, requestOptions); + this.responseType = Objects.requireNonNull(responseType, "parameter responseType cannot be null"); + } + + /** + * Builds the request to be executed + * + * @return the request to be executed + * @param requestOptions the options to apply to the request + */ + @Nonnull + public CustomRequest buildRequest(@Nullable final com.microsoft.graph.options.Option... requestOptions) { + return buildRequest(getOptions(requestOptions)); + } + + /** + * Builds the request to be executed + * + * @return the request to be executed + * @param requestOptions the options to apply to the request + */ + @Nonnull + public CustomRequest buildRequest(@Nullable final List requestOptions) { + return new CustomRequest(getRequestUrl(), getClient(), requestOptions, responseType); + } +} diff --git a/src/main/java/com/microsoft/graph/core/DateOnly.java b/src/main/java/com/microsoft/graph/core/DateOnly.java new file mode 100644 index 000000000..e5a805092 --- /dev/null +++ b/src/main/java/com/microsoft/graph/core/DateOnly.java @@ -0,0 +1,112 @@ +package com.microsoft.graph.core; + + +import java.text.ParseException; +import java.util.Locale; +import java.util.Objects; + +import javax.annotation.Nullable; +import javax.annotation.Nonnull; + +/** + * A timezone-nonspecific date + */ +public class DateOnly { + + /** + * The year + */ + private final int mYear; + + /** + * The month + */ + private final int mMonth; + + /** + * The day + */ + private final int mDay; + + /** + * Constructs a timezone-nonspecific DateOnly + * + * @param dateStr date string of the form yyyy-mm-dd + * @return the parsed DateOnly instance + * @exception ParseException If there was a failure parsing the dateStr + */ + @Nullable + public static DateOnly parse(@Nonnull final String dateStr) throws ParseException { + Objects.requireNonNull(dateStr, "parameter dateStr cannot be null"); + // break the date up into its constituent parts + final String[] dateInfo = dateStr.split("-"); + + // validate the split date string + final int expectedLength = 3; + if (dateInfo.length != expectedLength) { + throw new ParseException( + "Expected datestring format 'yyyy-mm-dd' but found: " + dateStr, 0 + ); + } + + // array indices for date parsing + final int indYear = 0; + final int indMonth = 1; + final int indDay = 2; + + // unpack this array + int year = Integer.parseInt(dateInfo[indYear]); + int month = Integer.parseInt(dateInfo[indMonth]); + int day = Integer.parseInt(dateInfo[indDay]); + + return new DateOnly(year, month, day); + } + + /** + * Constructs a timezone-nonspecific DateOnly + * + * @param year the year + * @param month 1-indexed month value (Jan == 1) + * @param day day of the month + */ + public DateOnly(final int year, final int month, final int day) { + mYear = year; + mMonth = month; + mDay = day; + } + + /** + * Gets the year + * + * @return the year + */ + public int getYear() { + return mYear; + } + + /** + * Gets the month + * + * @return the month + */ + public int getMonth() { + return mMonth; + } + + /** + * Gets the day + * + * @return the day + */ + public int getDay() { + return mDay; + } + + @Override + public String toString() { + return String.format( + Locale.ROOT, + "%04d-%02d-%02d", mYear, mMonth, mDay + ); + } +} diff --git a/src/main/java/com/microsoft/graph/core/GraphErrorCodes.java b/src/main/java/com/microsoft/graph/core/GraphErrorCodes.java new file mode 100644 index 000000000..8a7cbaaf8 --- /dev/null +++ b/src/main/java/com/microsoft/graph/core/GraphErrorCodes.java @@ -0,0 +1,161 @@ +// ------------------------------------------------------------------------------ +// Copyright (c) 2017 Microsoft Corporation +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sub-license, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// ------------------------------------------------------------------------------ + +package com.microsoft.graph.core; + +/** + * The common Graph error codes + * @see https://docs.microsoft.com/en-us/graph/errors + */ +public enum GraphErrorCodes { + // Standard error codes. + /** The collar doesn't have permission to perform the action */ + ACCESS_DENIED, + /** The app or user has been throttled */ + ACTIVITY_LIMIT_REACHED, + /** No description available */ + ASYNC_TASK_FAILED, + /** No description available */ + ASYNC_TASK_NOT_COMPLETED, + /** No description available */ + AUTHENTICATION_CANCELLED, + /** No description available */ + AUTHENTICATION_FAILURE, + /** An unspecified error has occurred. */ + GENERAL_EXCEPTION, + /** The specified byte range is invalid or unavailable. */ + INVALID_RANGE, + /** The request is malformed or incorrect. */ + INVALID_REQUEST, + /** The resource could not be found. */ + ITEM_NOT_FOUND, + /** Malware was detected in the requested resource. */ + MALWARE_DETECTED, + /** The specified item name already exists. */ + NAME_ALREADY_EXISTS, + /** The action is not allowed by the system. */ + NOT_ALLOWED, + /** The request is not supported by the system. */ + NOT_SUPPORTED, + /** The user has reached their quota limit. */ + QUOTA_LIMIT_REACHED, + /** The resource being updated has changed since the caller last read it, usually an eTag mismatch. */ + RESOURCE_MODIFIED, + /** The delta token is no longer valid, and the app must reset the sync state. */ + RESYNC_REQUIRED, + /** The service is not available. Try the request again after a delay. There may be a Retry-After header. */ + SERVICE_NOT_AVAILABLE, + /** The application is sending too many requests to the service. Try the request again after a delay. There may be a Retry-After header. */ + TOO_MANY_REDIRECTS, + /** The caller is not authenticated. */ + UNAUTHENTICATED, + + // Extend error codes. + /** No description available */ + ACCESS_RESTRICTED, + /** Access restricted to the item's owner. */ + AUTHORIZATION_REQUEST_DENIED, + /** Failed to get a consistent delta snapshot. Try again later. */ + CANNOT_SNAPSHOT_TREE, + /** Max limit on the number of child items was reached. */ + CHILD_ITEM_COUNT_EXCEEDED, + /** No description available */ + ERROR_INVALID_ID_MALFORMED, + /** No description available */ + ERROR_INVALID_USER, + /** ETag does not match the current item's value. */ + ENTITY_TAG_DOES_NOT_MATCH, + /** Declared total size for this fragment is different from that of the upload session. */ + FRAGMENT_LENGTH_MISMATCH, + /** Uploaded fragment is out of order. */ + FRAGMENT_OUT_OF_ORDER, + /** Uploaded fragment overlaps with existing data. */ + FRAGMENT_OVERLAP, + /** Invalid accept type. */ + INVALID_ACCEPT_TYPE, + /** Invalid parameter format. */ + INVALID_PARAMETER_FORMAT, + /** Name contains invalid characters. */ + INVALID_PATH, + /** Invalid query option. */ + INVALID_QUERY_OPTION, + /** Invalid start index. */ + INVALID_START_INDEX, + /** Lock token does not match existing lock. */ + LOCK_MISMATCH, + /** There is currently no unexpired lock on the item. */ + LOCK_NOT_FOUND_OR_ALREADY_EXPIRED, + /** Lock Owner ID does not match provided ID. */ + LOCK_OWNER_MISMATCH, + /** ETag header is malformed. ETags must be quoted strings. */ + MALFORMED_ENTITY_TAG, + /** Max limit on number of Documents is reached. */ + MAX_DOCUMENT_COUNT_EXCEEDED, + /** Max file size exceeded. */ + MAX_FILE_SIZE_EXCEEDED, + /** Max limit on number of Folders is reached. */ + MAX_FOLDER_COUNT_EXCEEDED, + /** Max file size exceeded. */ + MAX_FRAGMENT_LENGTH_EXCEEDED, + /** Max limit on number of Items is reached. */ + MAX_ITEM_COUNT_EXCEEDED, + /** Max query length exceeded. */ + MAX_QUERY_LENGTH_EXCEEDED, + /** Maximum stream size exceeded. */ + MAX_STREAM_SIZE_EXCEEDED, + /** Parameter Exceeds Maximum Length. */ + PARAMETER_IS_TOO_LONG, + /** Parameter is smaller then minimum value. */ + PARAMETER_IS_TOO_SMALL, + /** Path exceeds maximum length. */ + PATH_IS_TOO_LONG, + /** Folder hierarchy depth limit reached. */ + PATH_TOO_DEEP, + /** Property not updateable. */ + PROPERTY_NOT_UPDATEABLE, + /** Resync required. Replace any local items with the server's version (including deletes) if you're sure that the service was up to date with your local changes when you last sync'd. Upload any local changes that the server doesn't know about. */ + RESYNC_APPLY_DIFFERENCES, + /** Resync required. Upload any local items that the service did not return, and upload any files that differ from the server's version (keeping both copies if you're not sure which one is more up-to-date). */ + RESYNC_UPLOAD_DIFFERENCES, + /** Resource is temporarily read-only. */ + SERVICE_READ_ONLY, + /** Too many requests. */ + THROTTLED_REQUEST, + /** Too many results requested. */ + TOO_MANY_RESULTS_REQUESTED, + /** Too many terms in the query. */ + TOO_MANY_TERMS_IN_QUERY, + /** Operation is not allowed because the number of affected items exceeds threshold. */ + TOTAL_AFFECTED_ITEM_COUNT_EXCEEDED, + /** Data truncation is not allowed. */ + TRUNCATION_NOT_ALLOWED, + /** Upload session failed. */ + UPLOAD_SESSION_FAILED, + /** Upload session incomplete. */ + UPLOAD_SESSION_INCOMPLETE, + /** Upload session not found. */ + UPLOAD_SESSION_NOT_FOUND, + /** This document is suspicious and may have a virus. */ + VIRUS_SUSPICIOUS, + /** Zero or fewer results requested. */ + ZERO_OR_FEWER_RESULTS_REQUESTED, +} diff --git a/src/main/java/com/microsoft/graph/core/IBaseClient.java b/src/main/java/com/microsoft/graph/core/IBaseClient.java new file mode 100644 index 000000000..c260831a8 --- /dev/null +++ b/src/main/java/com/microsoft/graph/core/IBaseClient.java @@ -0,0 +1,111 @@ +// ------------------------------------------------------------------------------ +// Copyright (c) 2017 Microsoft Corporation +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sub-license, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// ------------------------------------------------------------------------------ + +package com.microsoft.graph.core; + +import javax.annotation.Nullable; +import javax.annotation.Nonnull; + +import com.google.gson.JsonElement; +import com.microsoft.graph.content.BatchRequestBuilder; +import com.microsoft.graph.http.IHttpProvider; +import com.microsoft.graph.logger.ILogger; +import com.microsoft.graph.serializer.ISerializer; + +/** + * A client that communications with an OData service + * @param type of a request for the native http client + */ +public interface IBaseClient { + /** + * Gets the service root + * + * @return the service root + */ + @Nonnull + String getServiceRoot(); + + /** + * Sets the service root + * + * @param value the service root + */ + void setServiceRoot(@Nonnull final String value); + + /** + * Gets the HTTP provider + * + * @return the HTTP provider + */ + @Nullable + IHttpProvider getHttpProvider(); + + /** + * Gets the logger + * + * @return the logger + */ + @Nullable + ILogger getLogger(); + + /** + * Gets the serializer + * + * @return the serializer + */ + @Nullable + ISerializer getSerializer(); + + /** + * Gets a builder to execute a custom request + * + * @return the custom request builder + * @param url the url to send the request to + * @param responseType the class to deserialize the response to + * @param the type to deserialize the response to + */ + @Nonnull + CustomRequestBuilder customRequest(@Nonnull final String url, @Nonnull final Class responseType); + + /** + * Gets a builder to execute a custom request with a generic JSONObject response + * + * @return the custom request builder + * @param url the url to send the request to + */ + @Nonnull + CustomRequestBuilder customRequest(@Nonnull final String url); + + /** + * Get the batch request builder. + * @return a request builder to execute a batch. + */ + @Nonnull + public BatchRequestBuilder batch(); + + /** + * Gets the service SDK version if the service SDK is in use, null otherwise + * @return the service SDK version if the service SDK is in use, null otherwise + */ + @Nullable + String getServiceSDKVersion(); +} diff --git a/src/main/java/com/microsoft/graph/core/Multipart.java b/src/main/java/com/microsoft/graph/core/Multipart.java new file mode 100644 index 000000000..0600cac80 --- /dev/null +++ b/src/main/java/com/microsoft/graph/core/Multipart.java @@ -0,0 +1,268 @@ +package com.microsoft.graph.core; + +import java.io.ByteArrayOutputStream; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.math.BigInteger; +import java.security.SecureRandom; +import java.util.Map; +import java.util.Objects; + +import javax.annotation.Nullable; +import javax.annotation.Nonnull; + +import com.google.common.annotations.VisibleForTesting; +import com.microsoft.graph.options.HeaderOption; + +/** + * Helper for submitting multipart data + * + * This follows the Network Working Group's RFC + * on multipart/form-data posting format: + * https://www.ietf.org/rfc/rfc2388.txt + */ +public class Multipart { + private String boundary; + private static final String RETURN = "\r\n"; + private ByteArrayOutputStream out; + /** Default encoding for multi-part requests */ + public static final String MULTIPART_ENCODING = "US-ASCII"; + private String contentType = "multipart/form-data"; + + /** + * Create a new multipart object + */ + public Multipart() { + out = new ByteArrayOutputStream(); + boundary = "part_" + new BigInteger(130, new SecureRandom()).toString(); + } + + /** + * Get the multipart boundary for use in the request header + * @return the multipart boundary + */ + @Nonnull + public String getBoundary() { + return boundary; + } + + /** + * Set the multipart boundary for use in the request header + * @param boundary The multipart boundary + */ + public void setBoundary(@Nonnull final String boundary) { + this.boundary = Objects.requireNonNull(boundary, "parameter boundary cannot be null"); + } + + /** + * Get the contentType for use in the request header + * @return the multipart Content-Type + */ + @Nonnull + public String getContentType() { + return contentType; + } + + /** + * Set the contentType for use in the request header + * @param contentType The multipart Content-Type + */ + public void setContentType(@Nonnull final String contentType) { + this.contentType = Objects.requireNonNull(contentType, "parameter contentType cannot be null"); + } + + /** + * Get the Content-Type header to send the multipart request + * @return the multipart header option + */ + @Nonnull + public HeaderOption header() { + return new HeaderOption("Content-Type", contentType + "; boundary=\"" + boundary + "\""); + } + + private void writePartData(String partContent, byte[] byteArray) throws IOException{ + out.write(partContent.getBytes(MULTIPART_ENCODING)); + out.write(byteArray); + String returnContent = RETURN + RETURN; + out.write(returnContent.getBytes(MULTIPART_ENCODING)); + } + + /** + * Create content headers value and parameter + * @param name The content header name + * @param contentType The content header Content-Type + * @param filename The content header filename + * @return content header value and parameter string + */ + @VisibleForTesting + String createPartHeader(String name, String contentType, String filename) { + StringBuilder partContent = new StringBuilder(addBoundary()); + partContent.append("Content-Disposition: form-data"); + if(filename != null) { + if(name != null) + partContent.append("; name=\"").append(name).append("\"; filename=\"").append(filename).append("\""); + else + partContent.append("; filename=\"").append(filename).append("\""); + } + else if(name != null) + partContent.append("; name=\"").append(name).append("\""); + if(contentType != null) + partContent.append(RETURN).append("Content-Type: ").append(contentType); + partContent.append(RETURN).append(RETURN); + return partContent.toString(); + } + + /** + * Create content headers value and parameter + * @param contentValue The content header value + * @param contentDispParameter Map containing content paramter's key and value pair + * @return content header value and parameter string + */ + @Nonnull + public static String createContentHeaderValue(@Nonnull final String contentValue, @Nullable final Map contentDispParameter) { + final StringBuilder builder = new StringBuilder(contentValue); + + if(contentDispParameter != null) { + for(Map.Entry entry : contentDispParameter.entrySet()) + builder.append(";" + entry.getKey() + "=\"" + entry.getValue() + "\""); + } + return builder.toString(); + } + + /** + * Create content headers header-name, value and parameter string + * @param headers Map containing Header-name and header-value pair + */ + private String createPartHeader(Map headers) { + final StringBuilder builder = new StringBuilder(addBoundary()); + final String defaultPartContent = "Content-Disposition: form-data;" + RETURN + "Content-Type: " + contentType + RETURN + RETURN; + + if(headers != null) { + for(Map.Entry entry : headers.entrySet()) + builder.append(entry.getKey() +": "+entry.getValue() + RETURN); + builder.append(RETURN); + } else + builder.append(defaultPartContent); + return builder.toString(); + } + + /** + * Add multipart content headers and byte content + * @param name The multipart content name + * @param contentType The multipart Content-Type + * @param filename The multipart content file name + * @param byteArray The multipart byte content + * @throws IOException + */ + private void addData(String name, String contentType, String filename, byte[] byteArray) throws IOException { + String partContent = createPartHeader(name, contentType, filename); + writePartData(partContent, byteArray); + } + + /** + * Add a part to the multipart body + * @param name The name of the part + * @param contentType The MIME type (text/html, video/mp4, etc.) + * @param byteArray The byte[] contents of the resource + * @throws IOException Throws an exception if the output stream cannot be written to + */ + public void addFormData(@Nonnull final String name, @Nonnull final String contentType, @Nonnull final byte[] byteArray) throws IOException { + addData(name, contentType, null, byteArray); + } + + /** + * Add a part to the multipart body + * @param contentType The MIME type (text/html, video/mp4, etc.) + * @param byteArray The byte[] contents of the resource + * @throws IOException Throws an exception if the output stream cannot be written to + */ + public void addPart(@Nonnull final String contentType, @Nonnull final byte[] byteArray) throws IOException { + addData(null, contentType, null, byteArray); + } + + /** + * Add a part to the multipart body + * @param headers Map containing Header's header-name(eg: Content-Disposition, Content-Type, etc..) and header's value-parameter string + * @param content The byte[] contents of the resource + * @throws IOException Throws an exception if the output stream cannot be written to + */ + public void addPart(@Nonnull final Map headers, @Nonnull final byte[] content) throws IOException{ + final String partContent = createPartHeader(headers); + writePartData(partContent, content); + } + + /** + * Add an HTML part to the multipart body + * @param name The name of the part + * @param content The HTML body for the part + * @throws IOException Throws an exception if the output stream cannot be written to + */ + public void addHtmlPart(@Nonnull final String name, @Nonnull final byte[] content) throws IOException { + addFormData(name, "text/html", content); + } + + /** + * Add a file part to the multipart body + * @param name The name of the part + * @param contentType The MIME type of the file (application/pdf, video/mp4, etc.) + * @param file The file + * @throws IOException Throws an exception if the output stream cannot be written to + */ + public void addFilePart(@Nonnull final String name, @Nonnull final String contentType, @Nonnull final java.io.File file) throws IOException { + try(final InputStream fileStream = new FileInputStream(file)) { + final byte[] fileBytes = getByteArray(fileStream); + addData(name, contentType, file.getName(), fileBytes); + } + } + + /** + * Adds a boundary at the beginning of a new part + * @return The boundary + */ + private String addBoundary() { + return "--" + boundary + RETURN; + } + + /** + * Adds a boundary at the end of the multipart body + * @return The boundary + */ + private String addEnding() { + return "--" + boundary + "--"; + } + + /** + * Returns a full multipart body byte array + * @return The byte[] representation of the multipart object + * @throws IOException Throws an exception if the output stream cannot be written to + */ + @Nullable + public byte[] content() throws IOException { + ByteArrayOutputStream finalStream = out; + finalStream.write(addEnding().getBytes(MULTIPART_ENCODING)); + return finalStream.toByteArray(); + } + + /** + * Helper method to convert an InputStream to a byte[] + * @param in The input stream to convert + * @return The byte[] + * @throws IOException Throws an exception if the output stream cannot be written to + */ + private byte[] getByteArray(final InputStream in) throws IOException { + try (final ByteArrayOutputStream buffer = new ByteArrayOutputStream()){ + int nRead; + byte[] data = new byte[16384]; + try { + while ((nRead = in.read(data, 0, data.length)) != -1) { + buffer.write(data, 0, nRead); + } + } finally { + in.close(); + } + buffer.flush(); + return buffer.toByteArray(); + } + } +} diff --git a/src/main/java/com/microsoft/graph/core/TimeOfDay.java b/src/main/java/com/microsoft/graph/core/TimeOfDay.java new file mode 100644 index 000000000..bc87efe02 --- /dev/null +++ b/src/main/java/com/microsoft/graph/core/TimeOfDay.java @@ -0,0 +1,114 @@ +package com.microsoft.graph.core; + + +import javax.annotation.Nullable; +import javax.annotation.Nonnull; + +import java.math.BigDecimal; +import java.text.ParseException; +import java.util.Locale; +import java.util.Objects; + +/** + * The time of day + */ +public class TimeOfDay { + + /** + * The hour + */ + private final int mHour; + + /** + * The minute + */ + private final int mMinute; + + /** + * The second + */ + private final int mSecond; + + /** + * Constructs a timezone-nonspecific DateOnly + * + * @param timeStr time string of the form HH:mm:ss + * @return the parsed DateOnly instance + * @exception ParseException If there was a failure parsing the dateStr + */ + @Nullable + public static TimeOfDay parse(@Nonnull final String timeStr) throws ParseException { + Objects.requireNonNull(timeStr, "parameter timeStr cannot be null"); + // break the date up into its constituent parts + final String[] timeInfo = timeStr.split(":"); + + // validate the split date string + final int expectedLength = 3; + if (timeInfo.length != expectedLength) { + throw new ParseException( + "Expected time string format 'HH:mm:ss' but found: " + timeStr, 0 + ); + } + + // array indices for date parsing + final int indHour = 0; + final int indMinute = 1; + final int indSecond = 2; + + // unpack this array + final int hour = Integer.parseInt(timeInfo[indHour]); + final int minute = Integer.parseInt(timeInfo[indMinute]); + final int second = new BigDecimal(timeInfo[indSecond]).intValue(); + + return new TimeOfDay(hour, minute, second); + } + + /** + * Constructs a TimeOfDay object + * + * @param hour the hour + * @param minute the minute + * @param second the second + */ + public TimeOfDay(final int hour, final int minute, final int second) { + mHour = hour; + mMinute = minute; + mSecond = second; + } + + /** + * Gets the hour + * + * @return the hour + */ + public int getHour() { + return mHour; + } + + /** + * Gets the minute + * + * @return the minute + */ + public int getMinute() { + return mMinute; + } + + /** + * Gets the second + * + * @return the second + */ + public int getSecond() { + return mSecond; + } + + @Override + @Nonnull + public String toString() { + return String.format( + Locale.ROOT, + "%02d:%02d:%02d", mHour, mMinute, mSecond + ); + } +} diff --git a/src/main/java/com/microsoft/graph/http/BaseActionCollectionRequest.java b/src/main/java/com/microsoft/graph/http/BaseActionCollectionRequest.java new file mode 100644 index 000000000..a9c38b4ec --- /dev/null +++ b/src/main/java/com/microsoft/graph/http/BaseActionCollectionRequest.java @@ -0,0 +1,107 @@ +// ------------------------------------------------------------------------------ +// Copyright (c) 2020 Microsoft Corporation +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sub-license, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// ------------------------------------------------------------------------------ + +package com.microsoft.graph.http; + +import java.lang.NoSuchFieldException; +import java.lang.IllegalAccessException; +import java.util.List; +import javax.annotation.Nullable; +import javax.annotation.Nonnull; + +import com.microsoft.graph.core.ClientException; +import com.microsoft.graph.core.IBaseClient; +import com.microsoft.graph.options.Option; + +/** + * A request against a collection bound action + * + * @param the type of the object in the collection + * @param the response collection type + * @param the collection page type + */ +public abstract class BaseActionCollectionRequest, + T3 extends BaseCollectionPage>> extends BaseCollectionRequest { + + + /** + * Create the collection request + * + * @param requestUrl the URL to make the request against + * @param client the client which can issue the request + * @param options the options for this request + * @param responseCollectionClass the class for the response collection + * @param collectionPageClass the class for the collection page + * @param collectionRequestBuilderClass the class for the collection request builder + */ + public BaseActionCollectionRequest(@Nonnull final String requestUrl, + @Nonnull final IBaseClient client, + @Nullable final List options, + @Nonnull final Class responseCollectionClass, + @Nonnull final Class collectionPageClass, + @Nonnull final Class, T2, T3, ? extends BaseCollectionRequest>> collectionRequestBuilderClass) { + super(requestUrl, client, options, responseCollectionClass, collectionPageClass, collectionRequestBuilderClass); + } + + /** + * Invokes the method and calls the callback with the resulting collection of objects + * @return a future with the result + */ + @Nonnull + public java.util.concurrent.CompletableFuture postAsync() { + getBaseRequest().setHttpMethod(HttpMethod.POST); + Object bodyToSend = null; + try { + bodyToSend = this.getClass().getField("body").get(this); + } catch (NoSuchFieldException | IllegalAccessException ex) { + // this action doesn't body arguments, expected, no-op + } + return getBaseRequest() + .getClient() + .getHttpProvider() + .sendAsync(this, + responseCollectionClass, + bodyToSend) + .thenApply(r -> buildFromResponse(r)); + } + /** + * Invokes the method and returns the resulting collection of objects + * @return a collection of objects returned by the method + */ + @Nullable + public T3 post() throws ClientException { + getBaseRequest().setHttpMethod(HttpMethod.POST); + Object bodyToSend = null; + try { + bodyToSend = this.getClass().getField("body").get(this); + } catch (NoSuchFieldException | IllegalAccessException ex) { + // this action doesn't body arguments, expected, no-op + } + return buildFromResponse(getBaseRequest() + .getClient() + .getHttpProvider() + .send(this, + responseCollectionClass, + bodyToSend) + ); + } +} diff --git a/src/main/java/com/microsoft/graph/http/BaseActionCollectionRequestBuilder.java b/src/main/java/com/microsoft/graph/http/BaseActionCollectionRequestBuilder.java new file mode 100644 index 000000000..000cac4cd --- /dev/null +++ b/src/main/java/com/microsoft/graph/http/BaseActionCollectionRequestBuilder.java @@ -0,0 +1,41 @@ +// ------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT License. See License in the project root for license information. +// ------------------------------------------------------------------------------ + +package com.microsoft.graph.http; + +import com.microsoft.graph.core.IBaseClient; +import com.microsoft.graph.options.Option; + +import java.util.List; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +/** + * The base method request builder class used for POST actions + */ +public class BaseActionCollectionRequestBuilder, + T3 extends ICollectionResponse, + T4 extends BaseCollectionPage>, + T5 extends BaseCollectionRequest> extends BaseCollectionRequestBuilder { + + /** + * Constructs a new {@link BaseActionCollectionRequestBuilder} + * + * @param requestUrl the URL for the request + * @param client the {@link IBaseClient} for handling requests + * @param options {@link List} of {@link Option}s to add to this request + * @param requestBuilderClass the class for the request builder + * @param collectionRequestClass the class for the collection request + */ + public BaseActionCollectionRequestBuilder( + @Nonnull final String requestUrl, + @Nonnull final IBaseClient client, + @Nullable final List options, + @Nonnull final Class requestBuilderClass, + @Nonnull final Class collectionRequestClass + ) { + super(requestUrl, client, options, requestBuilderClass, collectionRequestClass); + } +} diff --git a/src/main/java/com/microsoft/graph/http/BaseActionRequestBuilder.java b/src/main/java/com/microsoft/graph/http/BaseActionRequestBuilder.java new file mode 100644 index 000000000..17aff1f98 --- /dev/null +++ b/src/main/java/com/microsoft/graph/http/BaseActionRequestBuilder.java @@ -0,0 +1,33 @@ +// ------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT License. See License in the project root for license information. +// ------------------------------------------------------------------------------ + +package com.microsoft.graph.http; + +import com.microsoft.graph.core.IBaseClient; +import com.microsoft.graph.options.Option; + +import java.util.List; + +import javax.annotation.Nullable; +import javax.annotation.Nonnull; + +/** + * The base method request builder class used for POST actions + */ +public class BaseActionRequestBuilder extends BaseRequestBuilder { + /** + * Constructs a new {@link BaseActionRequestBuilder} + * + * @param requestUrl the URL for the request + * @param client the {@link IBaseClient} for handling requests + * @param options {@link List} of {@link Option}s to add to this request + */ + public BaseActionRequestBuilder( + @Nonnull final String requestUrl, + @Nonnull final IBaseClient client, + @Nullable final List options + ) { + super(requestUrl, client, options); + } +} diff --git a/src/main/java/com/microsoft/graph/http/BaseCollectionPage.java b/src/main/java/com/microsoft/graph/http/BaseCollectionPage.java new file mode 100644 index 000000000..fce85908a --- /dev/null +++ b/src/main/java/com/microsoft/graph/http/BaseCollectionPage.java @@ -0,0 +1,137 @@ +// ------------------------------------------------------------------------------ +// Copyright (c) 2017 Microsoft Corporation +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sub-license, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// ------------------------------------------------------------------------------ + +package com.microsoft.graph.http; + +import com.google.gson.JsonObject; +import com.microsoft.graph.serializer.AdditionalDataManager; +import com.microsoft.graph.serializer.ISerializer; +import com.microsoft.graph.serializer.IJsonBackedObject; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import javax.annotation.Nullable; +import javax.annotation.Nonnull; + +/** + * A page of results from a collection + * + * @param the type of the item contained within the collection + */ +public class BaseCollectionPage> implements IJsonBackedObject { + + private AdditionalDataManager additionalDataManager = new AdditionalDataManager(this); + + /** + * The contents of this page + */ + private final List pageContents; + + /** + * The request builder for the next page + */ + private final T2 requestBuilder; + + /** + * A collection page for WorkforceIntegration + * + * @param response the serialized WorkforceIntegrationCollectionResponse from the service + * @param builder the request builder for the next collection page + */ + public BaseCollectionPage(@Nonnull final ICollectionResponse response, @Nullable final T2 builder) { + this(response.values(), builder, response.additionalDataManager()); + } + + /** + * Creates the collection page + * + * @param pageContents the contents of this page + * @param nextRequestBuilder the request builder for the next page + */ + public BaseCollectionPage(@Nonnull final List pageContents, @Nullable final T2 nextRequestBuilder) { + // CollectionPages are never directly modifiable, either 'update'/'delete' the specific child or 'add' the new + // object to the 'children' of the collection. + this.pageContents = Collections.unmodifiableList(pageContents == null ? new ArrayList() : pageContents); + requestBuilder = nextRequestBuilder; + } + + /** + * Creates the collection page + * + * @param pageContents the contents of this page + * @param nextRequestBuilder the request builder for the next page + * @param responseAdditionalData the additional data returned by the response + */ + public BaseCollectionPage(@Nonnull final List pageContents, @Nullable final T2 nextRequestBuilder, @Nonnull final AdditionalDataManager responseAdditionalData) { + this(pageContents, nextRequestBuilder); + this.additionalDataManager().putAll(responseAdditionalData); + } + + /** + * Gets the next page request builder + * + * @return the next page request builder + */ + @Nullable + public T2 getNextPage() { + return requestBuilder; + } + + /** + * Gets the current page + * + * @return the current page + */ + @Nonnull + public List getCurrentPage() { + return pageContents; + } + + /** + * Sets the raw JSON object + * + * @param serializer the serializer + * @param json the JSON object to set this object to + */ + public void setRawObject(@Nonnull final ISerializer serializer, @Nonnull final JsonObject json) { + } + + @Override + @Nullable + public final AdditionalDataManager additionalDataManager() { + return additionalDataManager; + } + + private final static String odataCountKey = "@odata.count"; + /** + * Returns the odata count value if it has been requested, null otherwise + * @return the odata count value if it has been requested, null otherwise + */ + @Nullable + public final Long getCount() { + return additionalDataManager == null || !additionalDataManager.containsKey(odataCountKey) || !additionalDataManager.get(odataCountKey).isJsonPrimitive() ? + null : + additionalDataManager.get(odataCountKey).getAsLong(); + } +} diff --git a/src/main/java/com/microsoft/graph/http/BaseCollectionReferenceRequestBuilder.java b/src/main/java/com/microsoft/graph/http/BaseCollectionReferenceRequestBuilder.java new file mode 100644 index 000000000..ae833394c --- /dev/null +++ b/src/main/java/com/microsoft/graph/http/BaseCollectionReferenceRequestBuilder.java @@ -0,0 +1,51 @@ +// ------------------------------------------------------------------------------ +// Copyright (c) 2017 Microsoft Corporation +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sub-license, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// ------------------------------------------------------------------------------ + +package com.microsoft.graph.http; + +import com.microsoft.graph.core.IBaseClient; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +/** + * A request builder + */ +public class BaseCollectionReferenceRequestBuilder, + T3 extends ICollectionResponse, + T4 extends BaseCollectionPage>, + T5 extends BaseEntityCollectionRequest> extends BaseCollectionRequestBuilder { + /** + * The request builder for this collection + * + * @param requestUrl the request URL + * @param client the service client + * @param requestOptions the options for this request + * @param requestBuilderClass the class for the request builder + * @param collectionRequestClass the class for the collection request + */ + public BaseCollectionReferenceRequestBuilder(@Nonnull final String requestUrl, @Nonnull final IBaseClient client, @Nullable final java.util.List requestOptions, + @Nonnull final Class requestBuilderClass, + @Nonnull final Class collectionRequestClass) { + super(requestUrl, client, requestOptions, requestBuilderClass, collectionRequestClass); + } +} diff --git a/src/main/java/com/microsoft/graph/http/BaseCollectionRequest.java b/src/main/java/com/microsoft/graph/http/BaseCollectionRequest.java new file mode 100644 index 000000000..694703cb2 --- /dev/null +++ b/src/main/java/com/microsoft/graph/http/BaseCollectionRequest.java @@ -0,0 +1,447 @@ +// ------------------------------------------------------------------------------ +// Copyright (c) 2017 Microsoft Corporation +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sub-license, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// ------------------------------------------------------------------------------ + +package com.microsoft.graph.http; + +import java.net.URL; +import java.lang.reflect.InvocationTargetException; +import java.util.Collections; +import java.util.List; +import java.util.Objects; + +import javax.annotation.Nullable; +import javax.annotation.Nonnull; + +import com.microsoft.graph.core.ClientException; +import com.microsoft.graph.core.IBaseClient; +import com.microsoft.graph.httpcore.middlewareoption.IShouldRedirect; +import com.microsoft.graph.httpcore.middlewareoption.IShouldRetry; +import com.microsoft.graph.options.FunctionOption; +import com.microsoft.graph.options.HeaderOption; +import com.microsoft.graph.options.Option; +import com.microsoft.graph.options.QueryOption; + +/** + * A request against a collection + * + * @param the type of the object in the collection + * @param the response collection type + * @param the collection page type + */ +public abstract class BaseCollectionRequest, + T3 extends BaseCollectionPage>> implements IHttpRequest { + + /** + * The base request for this collection request + */ + private final BaseRequest baseRequest; + + /** + * The class for the response collection + */ + protected final Class responseCollectionClass; + + /** + * The class for the collection page + */ + private final Class collectionPageClass; + + private final Class, T2, T3, ? extends BaseCollectionRequest>> collRequestBuilderClass; + + + /** + * Create the collection request + * + * @param requestUrl the URL to make the request against + * @param client the client which can issue the request + * @param options the options for this request + * @param responseCollectionClass the class for the response collection + * @param collectionPageClass the class for the collection page + * @param collectionRequestBuilderClass the class for the collection request builder + */ + public BaseCollectionRequest(@Nonnull final String requestUrl, + @Nonnull final IBaseClient client, + @Nullable final List options, + @Nonnull final Class responseCollectionClass, + @Nonnull final Class collectionPageClass, + @Nonnull final Class, T2, T3, ? extends BaseCollectionRequest>> collectionRequestBuilderClass) { + this.responseCollectionClass = Objects.requireNonNull(responseCollectionClass, "parameter responseCollectionClass cannot be null"); + this.collectionPageClass = Objects.requireNonNull(collectionPageClass, "parameter collectionPageClass cannot be null"); + this.collRequestBuilderClass = Objects.requireNonNull(collectionRequestBuilderClass, "parameter collectionRequestBuilderClass cannot be null"); + baseRequest = new BaseRequest(requestUrl, client, options, responseCollectionClass) {}; + } + + /** + * Send this request + * + * @return the response object + * @throws ClientException an exception occurs if there was an error while the request was sent + */ + @Nullable + protected T2 send() throws ClientException { + baseRequest.setHttpMethod(HttpMethod.GET); + return baseRequest.getClient().getHttpProvider().send(this, responseCollectionClass, null); + } + /** + * Send this request + * + * @return the response object + * @throws ClientException an exception occurs if there was an error while the request was sent + */ + @Nullable + protected java.util.concurrent.CompletableFuture sendAsync() throws ClientException { + baseRequest.setHttpMethod(HttpMethod.GET); + return baseRequest.getClient().getHttpProvider().sendAsync(this, responseCollectionClass, null); + } + + /** + * Deserializes the collection from the response object + * + * @param response the collection response + * @return the collection page + */ + @Nullable + public T3 buildFromResponse(@Nonnull final T2 response) { + Objects.requireNonNull(response, "parameter response cannot be null"); + try { + final Object builder = response.nextLink() == null ? null : this.collRequestBuilderClass + .getConstructor(String.class, IBaseClient.class, java.util.List.class) + .newInstance(response.nextLink(), getBaseRequest().getClient(), Collections.emptyList()); + final T3 page = (T3)this.collectionPageClass.getConstructor(response.getClass(), this.collRequestBuilderClass).newInstance(response, builder); + return page; + } catch(IllegalArgumentException | InstantiationException | IllegalAccessException | InvocationTargetException | NoSuchMethodException | SecurityException ex) { + throw new ClientException("Could not find the required class", ex); + } + } + + /** + * Gets the request URL + * + * @return the request URL + */ + @Override + @Nonnull + public URL getRequestUrl() { + return baseRequest.getRequestUrl(); + } + + /** + * Gets the HTTP method + * + * @return the HTTP method + */ + @Override + @Nullable + public HttpMethod getHttpMethod() { + return baseRequest.getHttpMethod(); + } + + /** + * Gets the headers + * + * @return the headers + */ + @Override + @Nullable + public List getHeaders() { + return baseRequest.getHeaders(); + } + + /** + * Adds a header to this request + * + * @param header the name of the header + * @param value the value of the header + */ + @Override + public void addHeader(@Nonnull final String header, @Nullable final String value) { + Objects.requireNonNull(header, "parameter header cannot be null"); + baseRequest.addHeader(header, value); + } + + /** + * Sets useCaches parameter to cache the response + * + * @param useCaches the value of useCaches + */ + @Override + public void setUseCaches(boolean useCaches) { + baseRequest.setUseCaches(useCaches); + } + + /** + * Gets useCaches parameter + * + * @return the value of useCaches + */ + @Override + public boolean getUseCaches() { + return baseRequest.getUseCaches(); + } + + /** + * Gets the full list of options for this request + * + * @return the full list of options for this request + */ + @Nullable + public List