2323import com .google .api .client .http .ByteArrayContent ;
2424import com .google .api .client .http .GenericUrl ;
2525import com .google .api .client .http .HttpContent ;
26+ import com .google .api .client .http .HttpMethods ;
2627import com .google .api .client .http .HttpRequest ;
2728import com .google .api .client .http .HttpRequestFactory ;
2829import com .google .api .client .http .HttpResponse ;
6768
6869// TODO (#1307): Find a way to consolidate all 3P API interfaces
6970public class GooglePhotosInterface {
70-
7171 public static final String ERROR_HASH_MISMATCH = "Hash mismatch" ;
7272 private static final String GOOG_ERROR_HASH_MISMATCH_LEGACY = "Checksum from header does not match received payload content." ;
7373 private static final String GOOG_ERROR_HASH_MISMATCH_UNIFIED = "User-provided checksum does not match received payload content." ;
7474
75+ private static final String GOOGPHOTOS_ALBUMS_PERMISSION_ERROR = "The caller does not have permission" ;
76+ private static final String GOOGPHOTOS_PHOTO_PERMISSION_ERROR = "Google Photos is disabled for the user" ;
77+
7578 private static final String BASE_URL = "https://photoslibrary.googleapis.com/v1/" ;
7679 private static final int ALBUM_PAGE_SIZE = 20 ; // TODO
7780 private static final int MEDIA_PAGE_SIZE = 50 ; // TODO
@@ -196,57 +199,42 @@ public BatchMediaItemResponse createPhotos(NewMediaItemUpload newMediaItemUpload
196199 private <T > T makeGetRequest (String url , Optional <Map <String , String >> parameters , Class <T > clazz )
197200 throws IOException , InvalidTokenException , PermissionDeniedException {
198201 HttpRequestFactory requestFactory = httpTransport .createRequestFactory ();
199- HttpRequest getRequest =
200- requestFactory .buildGetRequest (
201- new GenericUrl (url + "?" + generateParamsString (parameters )));
202202
203203 HttpResponse response ;
204204 try {
205- response = getRequest .execute ();
206- } catch (HttpResponseException e ) {
207- response =
208- handleHttpResponseException (
209- () ->
205+ response = makeHttpRequest (() ->
210206 requestFactory .buildGetRequest (
211- new GenericUrl (url + "?" + generateParamsString (parameters ))),
212- e );
207+ new GenericUrl (url + "?" + generateParamsString (parameters ))));
208+ } catch (UploadErrorException e ) {
209+ throw new IllegalStateException ("GET request unexpectedly produced Upload exception" , e );
213210 }
214211
215- Preconditions .checkState (response .getStatusCode () == 200 );
216212 String result =
217213 CharStreams .toString (new InputStreamReader (response .getContent (), StandardCharsets .UTF_8 ));
218214 return objectMapper .readValue (result , clazz );
219215 }
220216
221- public <T > T makePostRequest (String url , Optional <Map <String , String >> parameters ,
222- Optional <Map <String , String >> extraHeaders , HttpContent httpContent , Class <T > clazz )
217+ public <T > T makePostRequest (
218+ String url ,
219+ Optional <Map <String , String >> parameters ,
220+ Optional <Map <String , String >> extraHeaders ,
221+ HttpContent httpContent ,
222+ Class <T > clazz )
223223 throws IOException , InvalidTokenException , PermissionDeniedException , UploadErrorException {
224- // Wait for write permit before making request
225- writeRateLimiter .acquire ();
226-
227224 HttpRequestFactory requestFactory = httpTransport .createRequestFactory ();
228- HttpRequest postRequest =
225+ HttpResponse response = makeHttpRequest (() -> {
226+ // Wait for write permit before making request
227+ writeRateLimiter .acquire ();
228+
229+ HttpRequest postRequest =
229230 requestFactory .buildPostRequest (
230231 new GenericUrl (url + "?" + generateParamsString (parameters )), httpContent );
231- extraHeaders .ifPresent (stringStringMap -> stringStringMap .forEach (
232- (key , value ) -> postRequest .getHeaders ().set (key , value )));
233- postRequest .setReadTimeout (2 * 60000 ); // 2 minutes read timeout
234- HttpResponse response ;
235-
236- try {
237- response = postRequest .execute ();
238- } catch (HttpResponseException e ) {
239- maybeRethrowAsUploadError (e );
240-
241- response =
242- handleHttpResponseException (
243- () ->
244- requestFactory .buildPostRequest (
245- new GenericUrl (url + "?" + generateParamsString (parameters )), httpContent ),
246- e );
247- }
232+ extraHeaders .ifPresent (stringStringMap -> stringStringMap .forEach (
233+ (key , value ) -> postRequest .getHeaders ().set (key , value )));
234+ postRequest .setReadTimeout (2 * 60000 ); // 2 minutes read timeout
235+ return postRequest ;
236+ });
248237
249- Preconditions .checkState (response .getStatusCode () == 200 );
250238 String result =
251239 CharStreams .toString (new InputStreamReader (response .getContent (), StandardCharsets .UTF_8 ));
252240 if (clazz .isAssignableFrom (String .class )) {
@@ -256,55 +244,115 @@ public <T> T makePostRequest(String url, Optional<Map<String, String>> parameter
256244 }
257245 }
258246
259- /**
260- * Converting {@link HttpResponseException} to upload-related exceptions. Current this is only
261- * used for payload hash verifications.
262- *
263- * Note that making this a separate method to avoid polluting throw lists.
264- */
265- private void maybeRethrowAsUploadError (HttpResponseException e ) throws UploadErrorException {
266- if (e .getStatusCode () == 400 ) {
267- if (e .getContent ().contains (GOOG_ERROR_HASH_MISMATCH_LEGACY ) || e .getContent ()
268- .contains (GOOG_ERROR_HASH_MISMATCH_UNIFIED )) {
269- throw new UploadErrorException (ERROR_HASH_MISMATCH , e );
247+ private HttpResponse makeHttpRequest (SupplierWithIO <HttpRequest > httpRequest )
248+ throws IOException , InvalidTokenException , PermissionDeniedException , UploadErrorException {
249+
250+ HttpResponse response = null ;
251+ HttpRequest firstReq = httpRequest .getWithIO ();
252+ try {
253+ response = firstReq .execute ();
254+ } catch (HttpResponseException firstReqException ) {
255+ Optional <HttpResponse > maybeTokenRefreshedRetry = Optional .empty ();
256+ try {
257+ maybeTokenRefreshedRetry =
258+ maybeRetryWithFreshToken (httpRequest , firstReqException );
259+ } catch (HttpResponseException tokenRefreshedRetryException ) {
260+ rethrowForDtpStandards (
261+ tokenRefreshedRetryException .getStatusCode (),
262+ Optional .of (tokenRefreshedRetryException ),
263+ Optional .empty () /*maybeResponse*/ );
270264 }
271- // Delegate other 400 errors and non-400 errors to {@link #handleHttpResponseException}.
265+
266+ if (maybeTokenRefreshedRetry .isPresent ()) {
267+ response = maybeTokenRefreshedRetry .get ();
268+ } else {
269+ rethrowForDtpStandards (
270+ firstReqException .getStatusCode (),
271+ Optional .of (firstReqException ),
272+ Optional .empty () /*maybeResponse*/ );
273+ }
274+ }
275+ Preconditions .checkNotNull (
276+ response ,
277+ "bug? response should be set, else DTP error already thrown, but neither happened?" );
278+
279+ if (response .getStatusCode () != 200 ) {
280+ rethrowForDtpStandards (
281+ response .getStatusCode (),
282+ Optional .empty () /*maybeException*/ ,
283+ Optional .ofNullable (response ));
272284 }
285+
286+ return response ;
273287 }
274288
275- private HttpResponse handleHttpResponseException (
289+ private Optional < HttpResponse > maybeRetryWithFreshToken (
276290 SupplierWithIO <HttpRequest > httpRequest , HttpResponseException e )
277- throws IOException , InvalidTokenException , PermissionDeniedException {
291+ throws IOException , InvalidTokenException {
278292 // if the response is "unauthorized", refresh the token and try the request again
279293 final int statusCode = e .getStatusCode ();
280294
281- if (statusCode == 401 ) {
282- monitor .info (() -> String .format ("GooglePhotosInterface: Attempting to refresh authorization token due to HTTP response code=%s, %s\n " , statusCode , e ));
283- // if the credential refresh failed, let the error bubble up via the IOException that gets
284- // thrown
285- credential = credentialFactory .refreshCredential (credential );
286- monitor .info (() -> "GooglePhotosInterface: Refreshed authorization token successfully" );
295+ if (statusCode != 401 ) {
296+ return Optional .empty ();
297+ }
298+
299+ monitor .info (() -> String .format ("GooglePhotosInterface: Attempting to refresh authorization token due to HTTP response code=%s, %s\n " , statusCode , e ));
300+ // if the credential refresh failed, let the error bubble up via the IOException that gets
301+ // thrown
302+ credential = credentialFactory .refreshCredential (credential );
303+ monitor .info (() -> "GooglePhotosInterface: Refreshed authorization token successfully" );
287304
288- // if the second attempt throws an error, then something else is wrong, and we bubble up the
289- // response errors
290- return httpRequest .getWithIO ().execute ();
305+ // if the second attempt throws an error, then something else is wrong, and we bubble up the
306+ // response errors
307+ return Optional .of (httpRequest .getWithIO ().execute ());
308+ }
309+
310+ /**
311+ * Tries to throw a DTP-standard exception for a request that's failed, given whatever info we
312+ * have about the failure (HTTP response code, if nothing else) .
313+ */
314+ // TODO: jzacsh rework this class to interact with HTTP in an easily
315+ // loggable/testable/re-usable way, like we did with MicrosoftApiResponse.
316+ private void rethrowForDtpStandards (
317+ int statusCode ,
318+ Optional <HttpResponseException > maybeException ,
319+ Optional <HttpResponse > maybeResponse )
320+ throws IOException , InvalidTokenException , PermissionDeniedException , UploadErrorException {
321+ final String emptyServerMessage = "[no server message: have neither response nor exception]" ;
322+ final Optional <String > serverMessage ;
323+ if (maybeException .isPresent ()) {
324+ serverMessage = Optional .of (maybeException .get ().getContent ());
325+ } else if (maybeResponse .isPresent ()) {
326+ serverMessage = Optional .of (maybeResponse .get ().getStatusMessage ());
327+ } else {
328+ serverMessage = Optional .empty ();
291329 }
292- // "The caller does not have permission" is potential error for albums.
293- // "Google Photos is disabled for the user" is potential error for photos.
330+
294331 if (statusCode == 403 &&
295- (e .getContent ().contains ("The caller does not have permission" ) ||
296- e .getContent ().contains ("Google Photos is disabled for the user" ))) {
297- throw new PermissionDeniedException ("User permission to google photos was denied" , e );
298- } else {
299- // something else is wrong, bubble up the error
300- throw new IOException (
301- "Bad status code: "
302- + e .getStatusCode ()
303- + " Error: '"
304- + e .getStatusMessage ()
305- + "' Content: "
306- + e .getContent ());
332+ (serverMessage .orElse ("" ).contains (GOOGPHOTOS_ALBUMS_PERMISSION_ERROR ) ||
333+ serverMessage .orElse ("" ).contains (GOOGPHOTOS_PHOTO_PERMISSION_ERROR ))) {
334+ throw new PermissionDeniedException ("User permission to google photos was denied" , maybeException .orElse (null ));
307335 }
336+
337+ // Upload-related exceptions; currently this is only used for payload hash verifications.
338+ if (statusCode == 400 &&
339+ (serverMessage .orElse ("" ).contains (GOOG_ERROR_HASH_MISMATCH_LEGACY ) ||
340+ serverMessage .orElse ("" ).contains (GOOG_ERROR_HASH_MISMATCH_UNIFIED ))) {
341+ Throwable throwableForBadResponse =
342+ new IOException (String .format (
343+ "non-error HTTP response statusCode=%s: %s" ,
344+ statusCode ,
345+ serverMessage .orElse (emptyServerMessage )));
346+ Throwable cause = maybeException .map (e -> (Throwable ) e /*downcast*/ ).orElse (throwableForBadResponse );
347+ throw new UploadErrorException (ERROR_HASH_MISMATCH , cause );
348+ }
349+
350+ // something else is wrong, bubble up the error
351+ throw new IOException (
352+ String .format ("Bad HTTP response: status code=%s, Error='%s' Content: %s" ,
353+ statusCode ,
354+ maybeException .map (e -> e .getStatusMessage ()).orElse ("[no HTTP error]" ),
355+ serverMessage .orElse (emptyServerMessage )));
308356 }
309357
310358 private String generateParamsString (Optional <Map <String , String >> params ) {
0 commit comments