summaryrefslogtreecommitdiff
path: root/chromium/google_apis
diff options
context:
space:
mode:
authorAndras Becsi <andras.becsi@digia.com>2014-03-18 13:16:26 +0100
committerFrederik Gladhorn <frederik.gladhorn@digia.com>2014-03-20 15:55:39 +0100
commit3f0f86b0caed75241fa71c95a5d73bc0164348c5 (patch)
tree92b9fb00f2e9e90b0be2262093876d4f43b6cd13 /chromium/google_apis
parente90d7c4b152c56919d963987e2503f9909a666d2 (diff)
downloadqtwebengine-chromium-3f0f86b0caed75241fa71c95a5d73bc0164348c5.tar.gz
Update to new stable branch 1750
This also includes an updated ninja and chromium dependencies needed on Windows. Change-Id: Icd597d80ed3fa4425933c9f1334c3c2e31291c42 Reviewed-by: Zoltan Arvai <zarvai@inf.u-szeged.hu> Reviewed-by: Zeno Albisser <zeno.albisser@digia.com>
Diffstat (limited to 'chromium/google_apis')
-rw-r--r--chromium/google_apis/OWNERS1
-rw-r--r--chromium/google_apis/cup/client_update_protocol.cc6
-rw-r--r--chromium/google_apis/drive/DEPS3
-rw-r--r--chromium/google_apis/drive/OWNERS5
-rw-r--r--chromium/google_apis/drive/auth_service.cc239
-rw-r--r--chromium/google_apis/drive/auth_service.h84
-rw-r--r--chromium/google_apis/drive/auth_service_interface.h58
-rw-r--r--chromium/google_apis/drive/auth_service_observer.h23
-rw-r--r--chromium/google_apis/drive/base_requests.cc799
-rw-r--r--chromium/google_apis/drive/base_requests.h539
-rw-r--r--chromium/google_apis/drive/base_requests_server_unittest.cc131
-rw-r--r--chromium/google_apis/drive/base_requests_unittest.cc204
-rw-r--r--chromium/google_apis/drive/drive_api_parser.cc729
-rw-r--r--chromium/google_apis/drive/drive_api_parser.h857
-rw-r--r--chromium/google_apis/drive/drive_api_parser_unittest.cc304
-rw-r--r--chromium/google_apis/drive/drive_api_requests.cc743
-rw-r--r--chromium/google_apis/drive/drive_api_requests.h733
-rw-r--r--chromium/google_apis/drive/drive_api_requests_unittest.cc1647
-rw-r--r--chromium/google_apis/drive/drive_api_url_generator.cc183
-rw-r--r--chromium/google_apis/drive/drive_api_url_generator.h93
-rw-r--r--chromium/google_apis/drive/drive_api_url_generator_unittest.cc394
-rw-r--r--chromium/google_apis/drive/drive_common_callbacks.h62
-rw-r--r--chromium/google_apis/drive/drive_entry_kinds.h40
-rw-r--r--chromium/google_apis/drive/dummy_auth_service.cc43
-rw-r--r--chromium/google_apis/drive/dummy_auth_service.h43
-rw-r--r--chromium/google_apis/drive/gdata_contacts_requests.cc115
-rw-r--r--chromium/google_apis/drive/gdata_contacts_requests.h102
-rw-r--r--chromium/google_apis/drive/gdata_errorcode.cc93
-rw-r--r--chromium/google_apis/drive/gdata_errorcode.h46
-rw-r--r--chromium/google_apis/drive/gdata_wapi_parser.cc883
-rw-r--r--chromium/google_apis/drive/gdata_wapi_parser.h866
-rw-r--r--chromium/google_apis/drive/gdata_wapi_parser_unittest.cc389
-rw-r--r--chromium/google_apis/drive/gdata_wapi_requests.cc680
-rw-r--r--chromium/google_apis/drive/gdata_wapi_requests.h486
-rw-r--r--chromium/google_apis/drive/gdata_wapi_requests_unittest.cc1561
-rw-r--r--chromium/google_apis/drive/gdata_wapi_url_generator.cc250
-rw-r--r--chromium/google_apis/drive/gdata_wapi_url_generator.h140
-rw-r--r--chromium/google_apis/drive/gdata_wapi_url_generator_unittest.cc222
-rw-r--r--chromium/google_apis/drive/request_sender.cc105
-rw-r--r--chromium/google_apis/drive/request_sender.h111
-rw-r--r--chromium/google_apis/drive/request_sender_unittest.cc250
-rw-r--r--chromium/google_apis/drive/request_util.cc26
-rw-r--r--chromium/google_apis/drive/request_util.h23
-rw-r--r--chromium/google_apis/drive/request_util_unittest.cc22
-rw-r--r--chromium/google_apis/drive/task_util.cc21
-rw-r--r--chromium/google_apis/drive/task_util.h136
-rw-r--r--chromium/google_apis/drive/test_util.cc183
-rw-r--r--chromium/google_apis/drive/test_util.h299
-rw-r--r--chromium/google_apis/drive/time_util.cc170
-rw-r--r--chromium/google_apis/drive/time_util.h35
-rw-r--r--chromium/google_apis/drive/time_util_unittest.cc96
-rw-r--r--chromium/google_apis/gaia/fake_gaia.cc61
-rw-r--r--chromium/google_apis/gaia/fake_gaia.h20
-rw-r--r--chromium/google_apis/gaia/gaia_auth_consumer.h3
-rw-r--r--chromium/google_apis/gaia/gaia_auth_fetcher.cc34
-rw-r--r--chromium/google_apis/gaia/gaia_auth_fetcher.h8
-rw-r--r--chromium/google_apis/gaia/gaia_auth_fetcher_unittest.cc37
-rw-r--r--chromium/google_apis/gaia/gaia_auth_util.cc38
-rw-r--r--chromium/google_apis/gaia/gaia_auth_util.h5
-rw-r--r--chromium/google_apis/gaia/gaia_auth_util_unittest.cc34
-rw-r--r--chromium/google_apis/gaia/gaia_oauth_client.cc47
-rw-r--r--chromium/google_apis/gaia/gaia_oauth_client.h10
-rw-r--r--chromium/google_apis/gaia/gaia_oauth_client_unittest.cc16
-rw-r--r--chromium/google_apis/gaia/gaia_urls.cc19
-rw-r--r--chromium/google_apis/gaia/gaia_urls.h6
-rw-r--r--chromium/google_apis/gaia/google_service_auth_error.cc2
-rw-r--r--chromium/google_apis/gaia/oauth2_access_token_fetcher.cc131
-rw-r--r--chromium/google_apis/gaia/oauth2_access_token_fetcher.h12
-rw-r--r--chromium/google_apis/gaia/oauth2_access_token_fetcher_unittest.cc31
-rw-r--r--chromium/google_apis/gaia/oauth2_mint_token_flow.cc38
-rw-r--r--chromium/google_apis/gaia/oauth2_mint_token_flow.h4
-rw-r--r--chromium/google_apis/gaia/oauth2_token_service.cc16
-rw-r--r--chromium/google_apis/gaia/oauth2_token_service.h28
-rw-r--r--chromium/google_apis/gaia/oauth_request_signer.cc40
-rw-r--r--chromium/google_apis/gcm/DEPS14
-rw-r--r--chromium/google_apis/gcm/OWNERS3
-rw-r--r--chromium/google_apis/gcm/base/gcm_export.h29
-rw-r--r--chromium/google_apis/gcm/base/mcs_message.cc78
-rw-r--r--chromium/google_apis/gcm/base/mcs_message.h85
-rw-r--r--chromium/google_apis/gcm/base/mcs_message_unittest.cc92
-rw-r--r--chromium/google_apis/gcm/base/mcs_util.cc233
-rw-r--r--chromium/google_apis/gcm/base/mcs_util.h81
-rw-r--r--chromium/google_apis/gcm/base/mcs_util_unittest.cc82
-rw-r--r--chromium/google_apis/gcm/base/socket_stream.cc332
-rw-r--r--chromium/google_apis/gcm/base/socket_stream.h205
-rw-r--r--chromium/google_apis/gcm/base/socket_stream_unittest.cc406
-rw-r--r--chromium/google_apis/gcm/engine/connection_factory.cc12
-rw-r--r--chromium/google_apis/gcm/engine/connection_factory.h64
-rw-r--r--chromium/google_apis/gcm/engine/connection_factory_impl.cc205
-rw-r--r--chromium/google_apis/gcm/engine/connection_factory_impl.h101
-rw-r--r--chromium/google_apis/gcm/engine/connection_factory_impl_unittest.cc303
-rw-r--r--chromium/google_apis/gcm/engine/connection_handler.cc15
-rw-r--r--chromium/google_apis/gcm/engine/connection_handler.h63
-rw-r--r--chromium/google_apis/gcm/engine/connection_handler_impl.cc404
-rw-r--r--chromium/google_apis/gcm/engine/connection_handler_impl.h122
-rw-r--r--chromium/google_apis/gcm/engine/connection_handler_impl_unittest.cc628
-rw-r--r--chromium/google_apis/gcm/engine/fake_connection_factory.cc46
-rw-r--r--chromium/google_apis/gcm/engine/fake_connection_factory.h42
-rw-r--r--chromium/google_apis/gcm/engine/fake_connection_handler.cc86
-rw-r--r--chromium/google_apis/gcm/engine/fake_connection_handler.h74
-rw-r--r--chromium/google_apis/gcm/engine/mcs_client.cc659
-rw-r--r--chromium/google_apis/gcm/engine/mcs_client.h231
-rw-r--r--chromium/google_apis/gcm/engine/mcs_client_unittest.cc540
-rw-r--r--chromium/google_apis/gcm/engine/rmq_store.cc491
-rw-r--r--chromium/google_apis/gcm/engine/rmq_store.h102
-rw-r--r--chromium/google_apis/gcm/engine/rmq_store_unittest.cc303
-rw-r--r--chromium/google_apis/gcm/gcm.gyp126
-rw-r--r--chromium/google_apis/gcm/gcm_client.cc45
-rw-r--r--chromium/google_apis/gcm/gcm_client.h193
-rw-r--r--chromium/google_apis/gcm/gcm_client_impl.cc39
-rw-r--r--chromium/google_apis/gcm/gcm_client_impl.h39
-rw-r--r--chromium/google_apis/gcm/protocol/mcs.proto269
-rw-r--r--chromium/google_apis/gcm/tools/mcs_probe.cc372
-rw-r--r--chromium/google_apis/google_api_keys.cc12
-rw-r--r--chromium/google_apis/google_apis.gyp33
115 files changed, 23037 insertions, 135 deletions
diff --git a/chromium/google_apis/OWNERS b/chromium/google_apis/OWNERS
index 4975363007f..e3221f4c57c 100644
--- a/chromium/google_apis/OWNERS
+++ b/chromium/google_apis/OWNERS
@@ -1 +1,2 @@
joi@chromium.org
+rogerta@chromium.org
diff --git a/chromium/google_apis/cup/client_update_protocol.cc b/chromium/google_apis/cup/client_update_protocol.cc
index e730557164f..afde3ab46b5 100644
--- a/chromium/google_apis/cup/client_update_protocol.cc
+++ b/chromium/google_apis/cup/client_update_protocol.cc
@@ -122,8 +122,7 @@ std::vector<uint8> RsaPad(size_t rsa_key_size,
// needed. Call the standard Base64 encoder/decoder and then apply fixups.
std::string UrlSafeB64Encode(const std::vector<uint8>& data) {
std::string result;
- if (!base::Base64Encode(ByteVectorToSP(data), &result))
- return std::string();
+ base::Base64Encode(ByteVectorToSP(data), &result);
// Do an tr|+/|-_| on the output, and strip any '=' padding.
for (std::string::iterator it = result.begin(); it != result.end(); ++it) {
@@ -138,7 +137,7 @@ std::string UrlSafeB64Encode(const std::vector<uint8>& data) {
break;
}
}
- TrimString(result, "=", &result);
+ base::TrimString(result, "=", &result);
return result;
}
@@ -302,4 +301,3 @@ bool ClientUpdateProtocol::DeriveSharedKey(const std::vector<uint8>& source) {
return true;
}
-
diff --git a/chromium/google_apis/drive/DEPS b/chromium/google_apis/drive/DEPS
new file mode 100644
index 00000000000..5827c268b07
--- /dev/null
+++ b/chromium/google_apis/drive/DEPS
@@ -0,0 +1,3 @@
+include_rules = [
+ "+third_party/libxml",
+]
diff --git a/chromium/google_apis/drive/OWNERS b/chromium/google_apis/drive/OWNERS
new file mode 100644
index 00000000000..3db88c0b171
--- /dev/null
+++ b/chromium/google_apis/drive/OWNERS
@@ -0,0 +1,5 @@
+hashimoto@chromium.org
+hidehiko@chromium.org
+kinaba@chromium.org
+satorux@chromium.org
+yoshiki@chromium.org
diff --git a/chromium/google_apis/drive/auth_service.cc b/chromium/google_apis/drive/auth_service.cc
new file mode 100644
index 00000000000..18623686f94
--- /dev/null
+++ b/chromium/google_apis/drive/auth_service.cc
@@ -0,0 +1,239 @@
+// Copyright (c) 2012 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "google_apis/drive/auth_service.h"
+
+#include <string>
+#include <vector>
+
+#include "base/bind.h"
+#include "base/location.h"
+#include "base/message_loop/message_loop_proxy.h"
+#include "base/metrics/histogram.h"
+#include "google_apis/drive/auth_service_observer.h"
+#include "google_apis/gaia/google_service_auth_error.h"
+#include "net/url_request/url_request_context_getter.h"
+
+namespace google_apis {
+
+namespace {
+
+// Used for success ratio histograms. 0 for failure, 1 for success,
+// 2 for no connection (likely offline).
+const int kSuccessRatioHistogramFailure = 0;
+const int kSuccessRatioHistogramSuccess = 1;
+const int kSuccessRatioHistogramNoConnection = 2;
+const int kSuccessRatioHistogramTemporaryFailure = 3;
+const int kSuccessRatioHistogramMaxValue = 4; // The max value is exclusive.
+
+// OAuth2 authorization token retrieval request.
+class AuthRequest : public OAuth2TokenService::Consumer {
+ public:
+ AuthRequest(OAuth2TokenService* oauth2_token_service,
+ const std::string& account_id,
+ net::URLRequestContextGetter* url_request_context_getter,
+ const AuthStatusCallback& callback,
+ const std::vector<std::string>& scopes);
+ virtual ~AuthRequest();
+
+ private:
+ // Overridden from OAuth2TokenService::Consumer:
+ virtual void OnGetTokenSuccess(const OAuth2TokenService::Request* request,
+ const std::string& access_token,
+ const base::Time& expiration_time) OVERRIDE;
+ virtual void OnGetTokenFailure(const OAuth2TokenService::Request* request,
+ const GoogleServiceAuthError& error) OVERRIDE;
+
+ AuthStatusCallback callback_;
+ scoped_ptr<OAuth2TokenService::Request> request_;
+ base::ThreadChecker thread_checker_;
+
+ DISALLOW_COPY_AND_ASSIGN(AuthRequest);
+};
+
+AuthRequest::AuthRequest(
+ OAuth2TokenService* oauth2_token_service,
+ const std::string& account_id,
+ net::URLRequestContextGetter* url_request_context_getter,
+ const AuthStatusCallback& callback,
+ const std::vector<std::string>& scopes)
+ : callback_(callback) {
+ DCHECK(!callback_.is_null());
+ request_ = oauth2_token_service->
+ StartRequestWithContext(
+ account_id,
+ url_request_context_getter,
+ OAuth2TokenService::ScopeSet(scopes.begin(), scopes.end()),
+ this);
+}
+
+AuthRequest::~AuthRequest() {}
+
+// Callback for OAuth2AccessTokenFetcher on success. |access_token| is the token
+// used to start fetching user data.
+void AuthRequest::OnGetTokenSuccess(const OAuth2TokenService::Request* request,
+ const std::string& access_token,
+ const base::Time& expiration_time) {
+ DCHECK(thread_checker_.CalledOnValidThread());
+
+ UMA_HISTOGRAM_ENUMERATION("GData.AuthSuccess",
+ kSuccessRatioHistogramSuccess,
+ kSuccessRatioHistogramMaxValue);
+
+ callback_.Run(HTTP_SUCCESS, access_token);
+ delete this;
+}
+
+// Callback for OAuth2AccessTokenFetcher on failure.
+void AuthRequest::OnGetTokenFailure(const OAuth2TokenService::Request* request,
+ const GoogleServiceAuthError& error) {
+ DCHECK(thread_checker_.CalledOnValidThread());
+
+ LOG(WARNING) << "AuthRequest: token request using refresh token failed: "
+ << error.ToString();
+
+ // There are many ways to fail, but if the failure is due to connection,
+ // it's likely that the device is off-line. We treat the error differently
+ // so that the file manager works while off-line.
+ if (error.state() == GoogleServiceAuthError::CONNECTION_FAILED) {
+ UMA_HISTOGRAM_ENUMERATION("GData.AuthSuccess",
+ kSuccessRatioHistogramNoConnection,
+ kSuccessRatioHistogramMaxValue);
+ callback_.Run(GDATA_NO_CONNECTION, std::string());
+ } else if (error.state() == GoogleServiceAuthError::SERVICE_UNAVAILABLE) {
+ // Temporary auth error.
+ UMA_HISTOGRAM_ENUMERATION("GData.AuthSuccess",
+ kSuccessRatioHistogramTemporaryFailure,
+ kSuccessRatioHistogramMaxValue);
+ callback_.Run(HTTP_FORBIDDEN, std::string());
+ } else {
+ // Permanent auth error.
+ UMA_HISTOGRAM_ENUMERATION("GData.AuthSuccess",
+ kSuccessRatioHistogramFailure,
+ kSuccessRatioHistogramMaxValue);
+ callback_.Run(HTTP_UNAUTHORIZED, std::string());
+ }
+ delete this;
+}
+
+} // namespace
+
+AuthService::AuthService(
+ OAuth2TokenService* oauth2_token_service,
+ const std::string& account_id,
+ net::URLRequestContextGetter* url_request_context_getter,
+ const std::vector<std::string>& scopes)
+ : oauth2_token_service_(oauth2_token_service),
+ account_id_(account_id),
+ url_request_context_getter_(url_request_context_getter),
+ scopes_(scopes),
+ weak_ptr_factory_(this) {
+ DCHECK(oauth2_token_service);
+
+ // Get OAuth2 refresh token (if we have any) and register for its updates.
+ oauth2_token_service_->AddObserver(this);
+ has_refresh_token_ = oauth2_token_service_->RefreshTokenIsAvailable(
+ account_id_);
+}
+
+AuthService::~AuthService() {
+ oauth2_token_service_->RemoveObserver(this);
+}
+
+void AuthService::StartAuthentication(const AuthStatusCallback& callback) {
+ DCHECK(thread_checker_.CalledOnValidThread());
+ scoped_refptr<base::MessageLoopProxy> relay_proxy(
+ base::MessageLoopProxy::current());
+
+ if (HasAccessToken()) {
+ // We already have access token. Give it back to the caller asynchronously.
+ relay_proxy->PostTask(FROM_HERE,
+ base::Bind(callback, HTTP_SUCCESS, access_token_));
+ } else if (HasRefreshToken()) {
+ // We have refresh token, let's get an access token.
+ new AuthRequest(oauth2_token_service_,
+ account_id_,
+ url_request_context_getter_,
+ base::Bind(&AuthService::OnAuthCompleted,
+ weak_ptr_factory_.GetWeakPtr(),
+ callback),
+ scopes_);
+ } else {
+ relay_proxy->PostTask(FROM_HERE,
+ base::Bind(callback, GDATA_NOT_READY, std::string()));
+ }
+}
+
+bool AuthService::HasAccessToken() const {
+ return !access_token_.empty();
+}
+
+bool AuthService::HasRefreshToken() const {
+ return has_refresh_token_;
+}
+
+const std::string& AuthService::access_token() const {
+ return access_token_;
+}
+
+void AuthService::ClearAccessToken() {
+ access_token_.clear();
+}
+
+void AuthService::ClearRefreshToken() {
+ has_refresh_token_ = false;
+
+ FOR_EACH_OBSERVER(AuthServiceObserver,
+ observers_,
+ OnOAuth2RefreshTokenChanged());
+}
+
+void AuthService::OnAuthCompleted(const AuthStatusCallback& callback,
+ GDataErrorCode error,
+ const std::string& access_token) {
+ DCHECK(thread_checker_.CalledOnValidThread());
+ DCHECK(!callback.is_null());
+
+ if (error == HTTP_SUCCESS) {
+ access_token_ = access_token;
+ } else if (error == HTTP_UNAUTHORIZED) {
+ // Refreshing access token using the refresh token is failed with 401 error
+ // (HTTP_UNAUTHORIZED). This means the current refresh token is invalid for
+ // Drive, hence we clear the refresh token here to make HasRefreshToken()
+ // false, thus the invalidness is clearly observable.
+ // This is not for triggering refetch of the refresh token. UI should
+ // show some message to encourage user to log-off and log-in again in order
+ // to fetch new valid refresh token.
+ ClearRefreshToken();
+ }
+
+ callback.Run(error, access_token);
+}
+
+void AuthService::AddObserver(AuthServiceObserver* observer) {
+ observers_.AddObserver(observer);
+}
+
+void AuthService::RemoveObserver(AuthServiceObserver* observer) {
+ observers_.RemoveObserver(observer);
+}
+
+void AuthService::OnRefreshTokenAvailable(const std::string& account_id) {
+ OnHandleRefreshToken(true);
+}
+
+void AuthService::OnRefreshTokenRevoked(const std::string& account_id) {
+ OnHandleRefreshToken(false);
+}
+
+void AuthService::OnHandleRefreshToken(bool has_refresh_token) {
+ access_token_.clear();
+ has_refresh_token_ = has_refresh_token;
+
+ FOR_EACH_OBSERVER(AuthServiceObserver,
+ observers_,
+ OnOAuth2RefreshTokenChanged());
+}
+
+} // namespace google_apis
diff --git a/chromium/google_apis/drive/auth_service.h b/chromium/google_apis/drive/auth_service.h
new file mode 100644
index 00000000000..f055a1ccb31
--- /dev/null
+++ b/chromium/google_apis/drive/auth_service.h
@@ -0,0 +1,84 @@
+// Copyright (c) 2012 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef GOOGLE_APIS_DRIVE_AUTH_SERVICE_H_
+#define GOOGLE_APIS_DRIVE_AUTH_SERVICE_H_
+
+#include <string>
+#include <vector>
+
+#include "base/memory/weak_ptr.h"
+#include "base/observer_list.h"
+#include "base/threading/thread_checker.h"
+#include "google_apis/drive/auth_service_interface.h"
+#include "google_apis/gaia/oauth2_token_service.h"
+
+namespace net {
+class URLRequestContextGetter;
+}
+
+namespace google_apis {
+
+class AuthServiceObserver;
+
+// This class provides authentication for Google services.
+// It integrates specific service integration with OAuth2 stack
+// (OAuth2TokenService) and provides OAuth2 token refresh infrastructure.
+// All public functions must be called on UI thread.
+class AuthService : public AuthServiceInterface,
+ public OAuth2TokenService::Observer {
+ public:
+ // |url_request_context_getter| is used to perform authentication with
+ // URLFetcher.
+ //
+ // |scopes| specifies OAuth2 scopes.
+ AuthService(OAuth2TokenService* oauth2_token_service,
+ const std::string& account_id,
+ net::URLRequestContextGetter* url_request_context_getter,
+ const std::vector<std::string>& scopes);
+ virtual ~AuthService();
+
+ // Overriden from AuthServiceInterface:
+ virtual void AddObserver(AuthServiceObserver* observer) OVERRIDE;
+ virtual void RemoveObserver(AuthServiceObserver* observer) OVERRIDE;
+ virtual void StartAuthentication(const AuthStatusCallback& callback) OVERRIDE;
+ virtual bool HasAccessToken() const OVERRIDE;
+ virtual bool HasRefreshToken() const OVERRIDE;
+ virtual const std::string& access_token() const OVERRIDE;
+ virtual void ClearAccessToken() OVERRIDE;
+ virtual void ClearRefreshToken() OVERRIDE;
+
+ // Overridden from OAuth2TokenService::Observer:
+ virtual void OnRefreshTokenAvailable(const std::string& account_id) OVERRIDE;
+ virtual void OnRefreshTokenRevoked(const std::string& account_id) OVERRIDE;
+
+ private:
+ // Called when the state of the refresh token changes.
+ void OnHandleRefreshToken(bool has_refresh_token);
+
+ // Called when authentication request from StartAuthentication() is
+ // completed.
+ void OnAuthCompleted(const AuthStatusCallback& callback,
+ GDataErrorCode error,
+ const std::string& access_token);
+
+ OAuth2TokenService* oauth2_token_service_;
+ std::string account_id_;
+ scoped_refptr<net::URLRequestContextGetter> url_request_context_getter_;
+ bool has_refresh_token_;
+ std::string access_token_;
+ std::vector<std::string> scopes_;
+ ObserverList<AuthServiceObserver> observers_;
+ base::ThreadChecker thread_checker_;
+
+ // Note: This should remain the last member so it'll be destroyed and
+ // invalidate its weak pointers before any other members are destroyed.
+ base::WeakPtrFactory<AuthService> weak_ptr_factory_;
+
+ DISALLOW_COPY_AND_ASSIGN(AuthService);
+};
+
+} // namespace google_apis
+
+#endif // GOOGLE_APIS_DRIVE_AUTH_SERVICE_H_
diff --git a/chromium/google_apis/drive/auth_service_interface.h b/chromium/google_apis/drive/auth_service_interface.h
new file mode 100644
index 00000000000..40a1905d54d
--- /dev/null
+++ b/chromium/google_apis/drive/auth_service_interface.h
@@ -0,0 +1,58 @@
+// Copyright (c) 2012 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef GOOGLE_APIS_DRIVE_AUTH_SERVICE_INTERFACE_H_
+#define GOOGLE_APIS_DRIVE_AUTH_SERVICE_INTERFACE_H_
+
+#include <string>
+
+#include "base/callback_forward.h"
+#include "google_apis/drive/gdata_errorcode.h"
+
+namespace google_apis {
+
+class AuthServiceObserver;
+
+// Called when fetching of access token is complete.
+typedef base::Callback<void(GDataErrorCode error,
+ const std::string& access_token)>
+ AuthStatusCallback;
+
+// This defines an interface for the authentication service which is required
+// by authenticated requests (AuthenticatedRequestInterface).
+// All functions must be called on UI thread.
+class AuthServiceInterface {
+ public:
+ virtual ~AuthServiceInterface() {}
+
+ // Adds and removes the observer.
+ virtual void AddObserver(AuthServiceObserver* observer) = 0;
+ virtual void RemoveObserver(AuthServiceObserver* observer) = 0;
+
+ // Starts fetching OAuth2 access token from the refresh token.
+ // |callback| must not be null.
+ virtual void StartAuthentication(const AuthStatusCallback& callback) = 0;
+
+ // True if an OAuth2 access token is retrieved and believed to be fresh.
+ // The access token is used to access the Drive server.
+ virtual bool HasAccessToken() const = 0;
+
+ // True if an OAuth2 refresh token is present. Its absence means that user
+ // is not properly authenticated.
+ // The refresh token is used to get the access token.
+ virtual bool HasRefreshToken() const = 0;
+
+ // Returns OAuth2 access token.
+ virtual const std::string& access_token() const = 0;
+
+ // Clears OAuth2 access token.
+ virtual void ClearAccessToken() = 0;
+
+ // Clears OAuth2 refresh token.
+ virtual void ClearRefreshToken() = 0;
+};
+
+} // namespace google_apis
+
+#endif // GOOGLE_APIS_DRIVE_AUTH_SERVICE_INTERFACE_H_
diff --git a/chromium/google_apis/drive/auth_service_observer.h b/chromium/google_apis/drive/auth_service_observer.h
new file mode 100644
index 00000000000..2fefbf20f3c
--- /dev/null
+++ b/chromium/google_apis/drive/auth_service_observer.h
@@ -0,0 +1,23 @@
+// Copyright (c) 2012 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef GOOGLE_APIS_DRIVE_AUTH_SERVICE_OBSERVER_H_
+#define GOOGLE_APIS_DRIVE_AUTH_SERVICE_OBSERVER_H_
+
+namespace google_apis {
+
+// Interface for classes that need to observe events from AuthService.
+// All events are notified on UI thread.
+class AuthServiceObserver {
+ public:
+ // Triggered when a new OAuth2 refresh token is received from AuthService.
+ virtual void OnOAuth2RefreshTokenChanged() = 0;
+
+ protected:
+ virtual ~AuthServiceObserver() {}
+};
+
+} // namespace google_apis
+
+#endif // GOOGLE_APIS_DRIVE_AUTH_SERVICE_OBSERVER_H_
diff --git a/chromium/google_apis/drive/base_requests.cc b/chromium/google_apis/drive/base_requests.cc
new file mode 100644
index 00000000000..be64f783c24
--- /dev/null
+++ b/chromium/google_apis/drive/base_requests.cc
@@ -0,0 +1,799 @@
+// Copyright (c) 2012 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "google_apis/drive/base_requests.h"
+
+#include "base/json/json_reader.h"
+#include "base/location.h"
+#include "base/sequenced_task_runner.h"
+#include "base/strings/string_number_conversions.h"
+#include "base/strings/stringprintf.h"
+#include "base/task_runner_util.h"
+#include "base/values.h"
+#include "google_apis/drive/request_sender.h"
+#include "google_apis/drive/task_util.h"
+#include "net/base/io_buffer.h"
+#include "net/base/load_flags.h"
+#include "net/base/net_errors.h"
+#include "net/http/http_byte_range.h"
+#include "net/http/http_response_headers.h"
+#include "net/http/http_util.h"
+#include "net/url_request/url_fetcher.h"
+#include "net/url_request/url_request_status.h"
+
+using net::URLFetcher;
+
+namespace {
+
+// Template for optional OAuth2 authorization HTTP header.
+const char kAuthorizationHeaderFormat[] = "Authorization: Bearer %s";
+// Template for GData API version HTTP header.
+const char kGDataVersionHeader[] = "GData-Version: 3.0";
+
+// Maximum number of attempts for re-authentication per request.
+const int kMaxReAuthenticateAttemptsPerRequest = 1;
+
+// Template for initiate upload of both GData WAPI and Drive API v2.
+const char kUploadContentType[] = "X-Upload-Content-Type: ";
+const char kUploadContentLength[] = "X-Upload-Content-Length: ";
+const char kUploadResponseLocation[] = "location";
+
+// Template for upload data range of both GData WAPI and Drive API v2.
+const char kUploadContentRange[] = "Content-Range: bytes ";
+const char kUploadResponseRange[] = "range";
+
+// Parse JSON string to base::Value object.
+scoped_ptr<base::Value> ParseJsonInternal(const std::string& json) {
+ int error_code = -1;
+ std::string error_message;
+ scoped_ptr<base::Value> value(base::JSONReader::ReadAndReturnError(
+ json, base::JSON_PARSE_RFC, &error_code, &error_message));
+
+ if (!value.get()) {
+ std::string trimmed_json;
+ if (json.size() < 80) {
+ trimmed_json = json;
+ } else {
+ // Take the first 50 and the last 10 bytes.
+ trimmed_json = base::StringPrintf(
+ "%s [%s bytes] %s",
+ json.substr(0, 50).c_str(),
+ base::Uint64ToString(json.size() - 60).c_str(),
+ json.substr(json.size() - 10).c_str());
+ }
+ LOG(WARNING) << "Error while parsing entry response: " << error_message
+ << ", code: " << error_code << ", json:\n" << trimmed_json;
+ }
+ return value.Pass();
+}
+
+// Returns response headers as a string. Returns a warning message if
+// |url_fetcher| does not contain a valid response. Used only for debugging.
+std::string GetResponseHeadersAsString(
+ const URLFetcher* url_fetcher) {
+ // net::HttpResponseHeaders::raw_headers(), as the name implies, stores
+ // all headers in their raw format, i.e each header is null-terminated.
+ // So logging raw_headers() only shows the first header, which is probably
+ // the status line. GetNormalizedHeaders, on the other hand, will show all
+ // the headers, one per line, which is probably what we want.
+ std::string headers;
+ // Check that response code indicates response headers are valid (i.e. not
+ // malformed) before we retrieve the headers.
+ if (url_fetcher->GetResponseCode() == URLFetcher::RESPONSE_CODE_INVALID) {
+ headers.assign("Response headers are malformed!!");
+ } else {
+ url_fetcher->GetResponseHeaders()->GetNormalizedHeaders(&headers);
+ }
+ return headers;
+}
+
+bool IsSuccessfulResponseCode(int response_code) {
+ return 200 <= response_code && response_code <= 299;
+}
+
+} // namespace
+
+namespace google_apis {
+
+void ParseJson(base::TaskRunner* blocking_task_runner,
+ const std::string& json,
+ const ParseJsonCallback& callback) {
+ base::PostTaskAndReplyWithResult(
+ blocking_task_runner,
+ FROM_HERE,
+ base::Bind(&ParseJsonInternal, json),
+ callback);
+}
+
+//=========================== ResponseWriter ==================================
+ResponseWriter::ResponseWriter(base::SequencedTaskRunner* file_task_runner,
+ const base::FilePath& file_path,
+ const GetContentCallback& get_content_callback)
+ : get_content_callback_(get_content_callback),
+ weak_ptr_factory_(this) {
+ if (!file_path.empty()) {
+ file_writer_.reset(
+ new net::URLFetcherFileWriter(file_task_runner, file_path));
+ }
+}
+
+ResponseWriter::~ResponseWriter() {
+}
+
+void ResponseWriter::DisownFile() {
+ DCHECK(file_writer_);
+ file_writer_->DisownFile();
+}
+
+int ResponseWriter::Initialize(const net::CompletionCallback& callback) {
+ if (file_writer_)
+ return file_writer_->Initialize(callback);
+
+ data_.clear();
+ return net::OK;
+}
+
+int ResponseWriter::Write(net::IOBuffer* buffer,
+ int num_bytes,
+ const net::CompletionCallback& callback) {
+ if (!get_content_callback_.is_null()) {
+ get_content_callback_.Run(
+ HTTP_SUCCESS,
+ make_scoped_ptr(new std::string(buffer->data(), num_bytes)));
+ }
+
+ if (file_writer_) {
+ const int result = file_writer_->Write(
+ buffer, num_bytes,
+ base::Bind(&ResponseWriter::DidWrite,
+ weak_ptr_factory_.GetWeakPtr(),
+ make_scoped_refptr(buffer), callback));
+ if (result != net::ERR_IO_PENDING)
+ DidWrite(buffer, net::CompletionCallback(), result);
+ return result;
+ }
+
+ data_.append(buffer->data(), num_bytes);
+ return num_bytes;
+}
+
+int ResponseWriter::Finish(const net::CompletionCallback& callback) {
+ if (file_writer_)
+ return file_writer_->Finish(callback);
+
+ return net::OK;
+}
+
+void ResponseWriter::DidWrite(scoped_refptr<net::IOBuffer> buffer,
+ const net::CompletionCallback& callback,
+ int result) {
+ if (result > 0) {
+ // Even if file_writer_ is used, append the data to |data_|, so that it can
+ // be used to get error information in case of server side errors.
+ // The size limit is to avoid consuming too much redundant memory.
+ const size_t kMaxStringSize = 1024*1024;
+ if (data_.size() < kMaxStringSize) {
+ data_.append(buffer->data(), std::min(static_cast<size_t>(result),
+ kMaxStringSize - data_.size()));
+ }
+ }
+
+ if (!callback.is_null())
+ callback.Run(result);
+}
+
+//============================ UrlFetchRequestBase ===========================
+
+UrlFetchRequestBase::UrlFetchRequestBase(RequestSender* sender)
+ : re_authenticate_count_(0),
+ sender_(sender),
+ error_code_(GDATA_OTHER_ERROR),
+ weak_ptr_factory_(this) {
+}
+
+UrlFetchRequestBase::~UrlFetchRequestBase() {}
+
+void UrlFetchRequestBase::Start(const std::string& access_token,
+ const std::string& custom_user_agent,
+ const ReAuthenticateCallback& callback) {
+ DCHECK(CalledOnValidThread());
+ DCHECK(!access_token.empty());
+ DCHECK(!callback.is_null());
+ DCHECK(re_authenticate_callback_.is_null());
+
+ re_authenticate_callback_ = callback;
+
+ GURL url = GetURL();
+ if (url.is_empty()) {
+ // Error is found on generating the url. Send the error message to the
+ // callback, and then return immediately without trying to connect
+ // to the server.
+ RunCallbackOnPrematureFailure(GDATA_OTHER_ERROR);
+ return;
+ }
+ DVLOG(1) << "URL: " << url.spec();
+
+ URLFetcher::RequestType request_type = GetRequestType();
+ url_fetcher_.reset(
+ URLFetcher::Create(url, request_type, this));
+ url_fetcher_->SetRequestContext(sender_->url_request_context_getter());
+ // Always set flags to neither send nor save cookies.
+ url_fetcher_->SetLoadFlags(
+ net::LOAD_DO_NOT_SEND_COOKIES | net::LOAD_DO_NOT_SAVE_COOKIES |
+ net::LOAD_DISABLE_CACHE);
+
+ base::FilePath output_file_path;
+ GetContentCallback get_content_callback;
+ GetOutputFilePath(&output_file_path, &get_content_callback);
+ if (!get_content_callback.is_null())
+ get_content_callback = CreateRelayCallback(get_content_callback);
+ response_writer_ = new ResponseWriter(blocking_task_runner(),
+ output_file_path,
+ get_content_callback);
+ url_fetcher_->SaveResponseWithWriter(
+ scoped_ptr<net::URLFetcherResponseWriter>(response_writer_));
+
+ // Add request headers.
+ // Note that SetExtraRequestHeaders clears the current headers and sets it
+ // to the passed-in headers, so calling it for each header will result in
+ // only the last header being set in request headers.
+ if (!custom_user_agent.empty())
+ url_fetcher_->AddExtraRequestHeader("User-Agent: " + custom_user_agent);
+ url_fetcher_->AddExtraRequestHeader(kGDataVersionHeader);
+ url_fetcher_->AddExtraRequestHeader(
+ base::StringPrintf(kAuthorizationHeaderFormat, access_token.data()));
+ std::vector<std::string> headers = GetExtraRequestHeaders();
+ for (size_t i = 0; i < headers.size(); ++i) {
+ url_fetcher_->AddExtraRequestHeader(headers[i]);
+ DVLOG(1) << "Extra header: " << headers[i];
+ }
+
+ // Set upload data if available.
+ std::string upload_content_type;
+ std::string upload_content;
+ if (GetContentData(&upload_content_type, &upload_content)) {
+ url_fetcher_->SetUploadData(upload_content_type, upload_content);
+ } else {
+ base::FilePath local_file_path;
+ int64 range_offset = 0;
+ int64 range_length = 0;
+ if (GetContentFile(&local_file_path, &range_offset, &range_length,
+ &upload_content_type)) {
+ url_fetcher_->SetUploadFilePath(
+ upload_content_type,
+ local_file_path,
+ range_offset,
+ range_length,
+ blocking_task_runner());
+ } else {
+ // Even if there is no content data, UrlFetcher requires to set empty
+ // upload data string for POST, PUT and PATCH methods, explicitly.
+ // It is because that most requests of those methods have non-empty
+ // body, and UrlFetcher checks whether it is actually not forgotten.
+ if (request_type == URLFetcher::POST ||
+ request_type == URLFetcher::PUT ||
+ request_type == URLFetcher::PATCH) {
+ // Set empty upload content-type and upload content, so that
+ // the request will have no "Content-type: " header and no content.
+ url_fetcher_->SetUploadData(std::string(), std::string());
+ }
+ }
+ }
+
+ url_fetcher_->Start();
+}
+
+URLFetcher::RequestType UrlFetchRequestBase::GetRequestType() const {
+ return URLFetcher::GET;
+}
+
+std::vector<std::string> UrlFetchRequestBase::GetExtraRequestHeaders() const {
+ return std::vector<std::string>();
+}
+
+bool UrlFetchRequestBase::GetContentData(std::string* upload_content_type,
+ std::string* upload_content) {
+ return false;
+}
+
+bool UrlFetchRequestBase::GetContentFile(base::FilePath* local_file_path,
+ int64* range_offset,
+ int64* range_length,
+ std::string* upload_content_type) {
+ return false;
+}
+
+void UrlFetchRequestBase::GetOutputFilePath(
+ base::FilePath* local_file_path,
+ GetContentCallback* get_content_callback) {
+}
+
+void UrlFetchRequestBase::Cancel() {
+ response_writer_ = NULL;
+ url_fetcher_.reset(NULL);
+ RunCallbackOnPrematureFailure(GDATA_CANCELLED);
+ sender_->RequestFinished(this);
+}
+
+GDataErrorCode UrlFetchRequestBase::GetErrorCode() {
+ return error_code_;
+}
+
+bool UrlFetchRequestBase::CalledOnValidThread() {
+ return thread_checker_.CalledOnValidThread();
+}
+
+base::SequencedTaskRunner* UrlFetchRequestBase::blocking_task_runner() const {
+ return sender_->blocking_task_runner();
+}
+
+void UrlFetchRequestBase::OnProcessURLFetchResultsComplete() {
+ sender_->RequestFinished(this);
+}
+
+void UrlFetchRequestBase::OnURLFetchComplete(const URLFetcher* source) {
+ DVLOG(1) << "Response headers:\n" << GetResponseHeadersAsString(source);
+
+ // Determine error code.
+ error_code_ = static_cast<GDataErrorCode>(source->GetResponseCode());
+ if (!source->GetStatus().is_success()) {
+ switch (source->GetStatus().error()) {
+ case net::ERR_NETWORK_CHANGED:
+ error_code_ = GDATA_NO_CONNECTION;
+ break;
+ default:
+ error_code_ = GDATA_OTHER_ERROR;
+ }
+ }
+
+ // The server may return detailed error status in JSON.
+ // See https://developers.google.com/drive/handle-errors
+ if (!IsSuccessfulResponseCode(error_code_)) {
+ DVLOG(1) << response_writer_->data();
+
+ const char kErrorKey[] = "error";
+ const char kErrorErrorsKey[] = "errors";
+ const char kErrorReasonKey[] = "reason";
+ const char kErrorMessageKey[] = "message";
+ const char kErrorReasonRateLimitExceeded[] = "rateLimitExceeded";
+ const char kErrorReasonUserRateLimitExceeded[] = "userRateLimitExceeded";
+
+ scoped_ptr<base::Value> value(ParseJsonInternal(response_writer_->data()));
+ base::DictionaryValue* dictionary = NULL;
+ base::DictionaryValue* error = NULL;
+ if (value &&
+ value->GetAsDictionary(&dictionary) &&
+ dictionary->GetDictionaryWithoutPathExpansion(kErrorKey, &error)) {
+ // Get error message.
+ std::string message;
+ error->GetStringWithoutPathExpansion(kErrorMessageKey, &message);
+ DLOG(ERROR) << "code: " << error_code_ << ", message: " << message;
+
+ // Override the error code based on the reason of the first error.
+ base::ListValue* errors = NULL;
+ base::DictionaryValue* first_error = NULL;
+ if (error->GetListWithoutPathExpansion(kErrorErrorsKey, &errors) &&
+ errors->GetDictionary(0, &first_error)) {
+ std::string reason;
+ first_error->GetStringWithoutPathExpansion(kErrorReasonKey, &reason);
+ if (reason == kErrorReasonRateLimitExceeded ||
+ reason == kErrorReasonUserRateLimitExceeded)
+ error_code_ = HTTP_SERVICE_UNAVAILABLE;
+ }
+ }
+ }
+
+ // Handle authentication failure.
+ if (error_code_ == HTTP_UNAUTHORIZED) {
+ if (++re_authenticate_count_ <= kMaxReAuthenticateAttemptsPerRequest) {
+ // Reset re_authenticate_callback_ so Start() can be called again.
+ ReAuthenticateCallback callback = re_authenticate_callback_;
+ re_authenticate_callback_.Reset();
+ callback.Run(this);
+ return;
+ }
+
+ OnAuthFailed(error_code_);
+ return;
+ }
+
+ // Overridden by each specialization
+ ProcessURLFetchResults(source);
+}
+
+void UrlFetchRequestBase::OnAuthFailed(GDataErrorCode code) {
+ RunCallbackOnPrematureFailure(code);
+ sender_->RequestFinished(this);
+}
+
+base::WeakPtr<AuthenticatedRequestInterface>
+UrlFetchRequestBase::GetWeakPtr() {
+ return weak_ptr_factory_.GetWeakPtr();
+}
+
+//============================ EntryActionRequest ============================
+
+EntryActionRequest::EntryActionRequest(RequestSender* sender,
+ const EntryActionCallback& callback)
+ : UrlFetchRequestBase(sender),
+ callback_(callback) {
+ DCHECK(!callback_.is_null());
+}
+
+EntryActionRequest::~EntryActionRequest() {}
+
+void EntryActionRequest::ProcessURLFetchResults(const URLFetcher* source) {
+ callback_.Run(GetErrorCode());
+ OnProcessURLFetchResultsComplete();
+}
+
+void EntryActionRequest::RunCallbackOnPrematureFailure(GDataErrorCode code) {
+ callback_.Run(code);
+}
+
+//============================== GetDataRequest ==============================
+
+GetDataRequest::GetDataRequest(RequestSender* sender,
+ const GetDataCallback& callback)
+ : UrlFetchRequestBase(sender),
+ callback_(callback),
+ weak_ptr_factory_(this) {
+ DCHECK(!callback_.is_null());
+}
+
+GetDataRequest::~GetDataRequest() {}
+
+void GetDataRequest::ParseResponse(GDataErrorCode fetch_error_code,
+ const std::string& data) {
+ DCHECK(CalledOnValidThread());
+
+ VLOG(1) << "JSON received from " << GetURL().spec() << ": "
+ << data.size() << " bytes";
+ ParseJson(blocking_task_runner(),
+ data,
+ base::Bind(&GetDataRequest::OnDataParsed,
+ weak_ptr_factory_.GetWeakPtr(),
+ fetch_error_code));
+}
+
+void GetDataRequest::ProcessURLFetchResults(const URLFetcher* source) {
+ GDataErrorCode fetch_error_code = GetErrorCode();
+
+ switch (fetch_error_code) {
+ case HTTP_SUCCESS:
+ case HTTP_CREATED:
+ ParseResponse(fetch_error_code, response_writer()->data());
+ break;
+ default:
+ RunCallbackOnPrematureFailure(fetch_error_code);
+ OnProcessURLFetchResultsComplete();
+ break;
+ }
+}
+
+void GetDataRequest::RunCallbackOnPrematureFailure(
+ GDataErrorCode fetch_error_code) {
+ callback_.Run(fetch_error_code, scoped_ptr<base::Value>());
+}
+
+void GetDataRequest::OnDataParsed(GDataErrorCode fetch_error_code,
+ scoped_ptr<base::Value> value) {
+ DCHECK(CalledOnValidThread());
+
+ if (!value.get())
+ fetch_error_code = GDATA_PARSE_ERROR;
+
+ callback_.Run(fetch_error_code, value.Pass());
+ OnProcessURLFetchResultsComplete();
+}
+
+//========================= InitiateUploadRequestBase ========================
+
+InitiateUploadRequestBase::InitiateUploadRequestBase(
+ RequestSender* sender,
+ const InitiateUploadCallback& callback,
+ const std::string& content_type,
+ int64 content_length)
+ : UrlFetchRequestBase(sender),
+ callback_(callback),
+ content_type_(content_type),
+ content_length_(content_length) {
+ DCHECK(!callback_.is_null());
+ DCHECK(!content_type_.empty());
+ DCHECK_GE(content_length_, 0);
+}
+
+InitiateUploadRequestBase::~InitiateUploadRequestBase() {}
+
+void InitiateUploadRequestBase::ProcessURLFetchResults(
+ const URLFetcher* source) {
+ GDataErrorCode code = GetErrorCode();
+
+ std::string upload_location;
+ if (code == HTTP_SUCCESS) {
+ // Retrieve value of the first "Location" header.
+ source->GetResponseHeaders()->EnumerateHeader(NULL,
+ kUploadResponseLocation,
+ &upload_location);
+ }
+
+ callback_.Run(code, GURL(upload_location));
+ OnProcessURLFetchResultsComplete();
+}
+
+void InitiateUploadRequestBase::RunCallbackOnPrematureFailure(
+ GDataErrorCode code) {
+ callback_.Run(code, GURL());
+}
+
+std::vector<std::string>
+InitiateUploadRequestBase::GetExtraRequestHeaders() const {
+ std::vector<std::string> headers;
+ headers.push_back(kUploadContentType + content_type_);
+ headers.push_back(
+ kUploadContentLength + base::Int64ToString(content_length_));
+ return headers;
+}
+
+//============================ UploadRangeResponse =============================
+
+UploadRangeResponse::UploadRangeResponse()
+ : code(HTTP_SUCCESS),
+ start_position_received(0),
+ end_position_received(0) {
+}
+
+UploadRangeResponse::UploadRangeResponse(GDataErrorCode code,
+ int64 start_position_received,
+ int64 end_position_received)
+ : code(code),
+ start_position_received(start_position_received),
+ end_position_received(end_position_received) {
+}
+
+UploadRangeResponse::~UploadRangeResponse() {
+}
+
+//========================== UploadRangeRequestBase ==========================
+
+UploadRangeRequestBase::UploadRangeRequestBase(RequestSender* sender,
+ const GURL& upload_url)
+ : UrlFetchRequestBase(sender),
+ upload_url_(upload_url),
+ weak_ptr_factory_(this) {
+}
+
+UploadRangeRequestBase::~UploadRangeRequestBase() {}
+
+GURL UploadRangeRequestBase::GetURL() const {
+ // This is very tricky to get json from this request. To do that, &alt=json
+ // has to be appended not here but in InitiateUploadRequestBase::GetURL().
+ return upload_url_;
+}
+
+URLFetcher::RequestType UploadRangeRequestBase::GetRequestType() const {
+ return URLFetcher::PUT;
+}
+
+void UploadRangeRequestBase::ProcessURLFetchResults(
+ const URLFetcher* source) {
+ GDataErrorCode code = GetErrorCode();
+ net::HttpResponseHeaders* hdrs = source->GetResponseHeaders();
+
+ if (code == HTTP_RESUME_INCOMPLETE) {
+ // Retrieve value of the first "Range" header.
+ // The Range header is appeared only if there is at least one received
+ // byte. So, initialize the positions by 0 so that the [0,0) will be
+ // returned via the |callback_| for empty data case.
+ int64 start_position_received = 0;
+ int64 end_position_received = 0;
+ std::string range_received;
+ hdrs->EnumerateHeader(NULL, kUploadResponseRange, &range_received);
+ if (!range_received.empty()) { // Parse the range header.
+ std::vector<net::HttpByteRange> ranges;
+ if (net::HttpUtil::ParseRangeHeader(range_received, &ranges) &&
+ !ranges.empty() ) {
+ // We only care about the first start-end pair in the range.
+ //
+ // Range header represents the range inclusively, while we are treating
+ // ranges exclusively (i.e., end_position_received should be one passed
+ // the last valid index). So "+ 1" is added.
+ start_position_received = ranges[0].first_byte_position();
+ end_position_received = ranges[0].last_byte_position() + 1;
+ }
+ }
+ // The Range header has the received data range, so the start position
+ // should be always 0.
+ DCHECK_EQ(start_position_received, 0);
+
+ OnRangeRequestComplete(UploadRangeResponse(code,
+ start_position_received,
+ end_position_received),
+ scoped_ptr<base::Value>());
+
+ OnProcessURLFetchResultsComplete();
+ } else if (code == HTTP_CREATED || code == HTTP_SUCCESS) {
+ // The upload is successfully done. Parse the response which should be
+ // the entry's metadata.
+ ParseJson(blocking_task_runner(),
+ response_writer()->data(),
+ base::Bind(&UploadRangeRequestBase::OnDataParsed,
+ weak_ptr_factory_.GetWeakPtr(),
+ code));
+ } else {
+ // Failed to upload. Run callbacks to notify the error.
+ OnRangeRequestComplete(
+ UploadRangeResponse(code, -1, -1), scoped_ptr<base::Value>());
+ OnProcessURLFetchResultsComplete();
+ }
+}
+
+void UploadRangeRequestBase::OnDataParsed(GDataErrorCode code,
+ scoped_ptr<base::Value> value) {
+ DCHECK(CalledOnValidThread());
+ DCHECK(code == HTTP_CREATED || code == HTTP_SUCCESS);
+
+ OnRangeRequestComplete(UploadRangeResponse(code, -1, -1), value.Pass());
+ OnProcessURLFetchResultsComplete();
+}
+
+void UploadRangeRequestBase::RunCallbackOnPrematureFailure(
+ GDataErrorCode code) {
+ OnRangeRequestComplete(
+ UploadRangeResponse(code, 0, 0), scoped_ptr<base::Value>());
+}
+
+//========================== ResumeUploadRequestBase =========================
+
+ResumeUploadRequestBase::ResumeUploadRequestBase(
+ RequestSender* sender,
+ const GURL& upload_location,
+ int64 start_position,
+ int64 end_position,
+ int64 content_length,
+ const std::string& content_type,
+ const base::FilePath& local_file_path)
+ : UploadRangeRequestBase(sender, upload_location),
+ start_position_(start_position),
+ end_position_(end_position),
+ content_length_(content_length),
+ content_type_(content_type),
+ local_file_path_(local_file_path) {
+ DCHECK_LE(start_position_, end_position_);
+}
+
+ResumeUploadRequestBase::~ResumeUploadRequestBase() {}
+
+std::vector<std::string>
+ResumeUploadRequestBase::GetExtraRequestHeaders() const {
+ if (content_length_ == 0) {
+ // For uploading an empty document, just PUT an empty content.
+ DCHECK_EQ(start_position_, 0);
+ DCHECK_EQ(end_position_, 0);
+ return std::vector<std::string>();
+ }
+
+ // The header looks like
+ // Content-Range: bytes <start_position>-<end_position>/<content_length>
+ // for example:
+ // Content-Range: bytes 7864320-8388607/13851821
+ // The header takes inclusive range, so we adjust by "end_position - 1".
+ DCHECK_GE(start_position_, 0);
+ DCHECK_GT(end_position_, 0);
+ DCHECK_GE(content_length_, 0);
+
+ std::vector<std::string> headers;
+ headers.push_back(
+ std::string(kUploadContentRange) +
+ base::Int64ToString(start_position_) + "-" +
+ base::Int64ToString(end_position_ - 1) + "/" +
+ base::Int64ToString(content_length_));
+ return headers;
+}
+
+bool ResumeUploadRequestBase::GetContentFile(
+ base::FilePath* local_file_path,
+ int64* range_offset,
+ int64* range_length,
+ std::string* upload_content_type) {
+ if (start_position_ == end_position_) {
+ // No content data.
+ return false;
+ }
+
+ *local_file_path = local_file_path_;
+ *range_offset = start_position_;
+ *range_length = end_position_ - start_position_;
+ *upload_content_type = content_type_;
+ return true;
+}
+
+//======================== GetUploadStatusRequestBase ========================
+
+GetUploadStatusRequestBase::GetUploadStatusRequestBase(RequestSender* sender,
+ const GURL& upload_url,
+ int64 content_length)
+ : UploadRangeRequestBase(sender, upload_url),
+ content_length_(content_length) {}
+
+GetUploadStatusRequestBase::~GetUploadStatusRequestBase() {}
+
+std::vector<std::string>
+GetUploadStatusRequestBase::GetExtraRequestHeaders() const {
+ // The header looks like
+ // Content-Range: bytes */<content_length>
+ // for example:
+ // Content-Range: bytes */13851821
+ DCHECK_GE(content_length_, 0);
+
+ std::vector<std::string> headers;
+ headers.push_back(
+ std::string(kUploadContentRange) + "*/" +
+ base::Int64ToString(content_length_));
+ return headers;
+}
+
+//============================ DownloadFileRequestBase =========================
+
+DownloadFileRequestBase::DownloadFileRequestBase(
+ RequestSender* sender,
+ const DownloadActionCallback& download_action_callback,
+ const GetContentCallback& get_content_callback,
+ const ProgressCallback& progress_callback,
+ const GURL& download_url,
+ const base::FilePath& output_file_path)
+ : UrlFetchRequestBase(sender),
+ download_action_callback_(download_action_callback),
+ get_content_callback_(get_content_callback),
+ progress_callback_(progress_callback),
+ download_url_(download_url),
+ output_file_path_(output_file_path) {
+ DCHECK(!download_action_callback_.is_null());
+ DCHECK(!output_file_path_.empty());
+ // get_content_callback may be null.
+}
+
+DownloadFileRequestBase::~DownloadFileRequestBase() {}
+
+// Overridden from UrlFetchRequestBase.
+GURL DownloadFileRequestBase::GetURL() const {
+ return download_url_;
+}
+
+void DownloadFileRequestBase::GetOutputFilePath(
+ base::FilePath* local_file_path,
+ GetContentCallback* get_content_callback) {
+ // Configure so that the downloaded content is saved to |output_file_path_|.
+ *local_file_path = output_file_path_;
+ *get_content_callback = get_content_callback_;
+}
+
+void DownloadFileRequestBase::OnURLFetchDownloadProgress(
+ const URLFetcher* source,
+ int64 current,
+ int64 total) {
+ if (!progress_callback_.is_null())
+ progress_callback_.Run(current, total);
+}
+
+void DownloadFileRequestBase::ProcessURLFetchResults(const URLFetcher* source) {
+ GDataErrorCode code = GetErrorCode();
+
+ // Take over the ownership of the the downloaded temp file.
+ base::FilePath temp_file;
+ if (code == HTTP_SUCCESS) {
+ response_writer()->DisownFile();
+ temp_file = output_file_path_;
+ }
+
+ download_action_callback_.Run(code, temp_file);
+ OnProcessURLFetchResultsComplete();
+}
+
+void DownloadFileRequestBase::RunCallbackOnPrematureFailure(
+ GDataErrorCode code) {
+ download_action_callback_.Run(code, base::FilePath());
+}
+
+} // namespace google_apis
diff --git a/chromium/google_apis/drive/base_requests.h b/chromium/google_apis/drive/base_requests.h
new file mode 100644
index 00000000000..afe12a04fbe
--- /dev/null
+++ b/chromium/google_apis/drive/base_requests.h
@@ -0,0 +1,539 @@
+// Copyright (c) 2012 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+//
+// This file provides base classes used to issue HTTP requests for Google
+// APIs.
+
+#ifndef GOOGLE_APIS_DRIVE_BASE_REQUESTS_H_
+#define GOOGLE_APIS_DRIVE_BASE_REQUESTS_H_
+
+#include <string>
+#include <vector>
+
+#include "base/callback.h"
+#include "base/files/file_path.h"
+#include "base/memory/weak_ptr.h"
+#include "base/threading/thread_checker.h"
+#include "google_apis/drive/gdata_errorcode.h"
+#include "net/url_request/url_fetcher.h"
+#include "net/url_request/url_fetcher_delegate.h"
+#include "net/url_request/url_fetcher_response_writer.h"
+#include "url/gurl.h"
+
+namespace base {
+class Value;
+} // namespace base
+
+namespace google_apis {
+
+class RequestSender;
+
+// Callback used to pass parsed JSON from ParseJson(). If parsing error occurs,
+// then the passed argument is null.
+typedef base::Callback<void(scoped_ptr<base::Value> value)> ParseJsonCallback;
+
+// Callback used for DownloadFileRequest and ResumeUploadRequestBase.
+typedef base::Callback<void(int64 progress, int64 total)> ProgressCallback;
+
+// Callback used to get the content from DownloadFileRequest.
+typedef base::Callback<void(
+ GDataErrorCode error,
+ scoped_ptr<std::string> content)> GetContentCallback;
+
+// Parses JSON passed in |json| on |blocking_task_runner|. Runs |callback| on
+// the calling thread when finished with either success or failure.
+// The callback must not be null.
+void ParseJson(base::TaskRunner* blocking_task_runner,
+ const std::string& json,
+ const ParseJsonCallback& callback);
+
+//======================= AuthenticatedRequestInterface ======================
+
+// An interface class for implementing a request which requires OAuth2
+// authentication.
+class AuthenticatedRequestInterface {
+ public:
+ // Called when re-authentication is required. See Start() for details.
+ typedef base::Callback<void(AuthenticatedRequestInterface* request)>
+ ReAuthenticateCallback;
+
+ virtual ~AuthenticatedRequestInterface() {}
+
+ // Starts the request with |access_token|. User-Agent header will be set
+ // to |custom_user_agent| if the value is not empty.
+ //
+ // |callback| is called when re-authentication is needed for a certain
+ // number of times (see kMaxReAuthenticateAttemptsPerRequest in .cc).
+ // The callback should retry by calling Start() again with a new access
+ // token, or just call OnAuthFailed() if a retry is not attempted.
+ // |callback| must not be null.
+ virtual void Start(const std::string& access_token,
+ const std::string& custom_user_agent,
+ const ReAuthenticateCallback& callback) = 0;
+
+ // Invoked when the authentication failed with an error code |code|.
+ virtual void OnAuthFailed(GDataErrorCode code) = 0;
+
+ // Gets a weak pointer to this request object. Since requests may be
+ // deleted when it is canceled by user action, for posting asynchronous tasks
+ // on the authentication request object, weak pointers have to be used.
+ // TODO(kinaba): crbug.com/134814 use more clean life time management than
+ // using weak pointers.
+ virtual base::WeakPtr<AuthenticatedRequestInterface> GetWeakPtr() = 0;
+
+ // Cancels the request. It will invoke the callback object passed in
+ // each request's constructor with error code GDATA_CANCELLED.
+ virtual void Cancel() = 0;
+};
+
+//=========================== ResponseWriter ==================================
+
+// Saves the response for the request to a file or string.
+class ResponseWriter : public net::URLFetcherResponseWriter {
+ public:
+ // If file_path is not empty, the response will be saved with file_writer_,
+ // otherwise it will be saved to data_.
+ ResponseWriter(base::SequencedTaskRunner* file_task_runner,
+ const base::FilePath& file_path,
+ const GetContentCallback& get_content_callback);
+ virtual ~ResponseWriter();
+
+ const std::string& data() const { return data_; }
+
+ // Disowns the output file.
+ void DisownFile();
+
+ // URLFetcherResponseWriter overrides:
+ virtual int Initialize(const net::CompletionCallback& callback) OVERRIDE;
+ virtual int Write(net::IOBuffer* buffer,
+ int num_bytes,
+ const net::CompletionCallback& callback) OVERRIDE;
+ virtual int Finish(const net::CompletionCallback& callback) OVERRIDE;
+
+ private:
+ void DidWrite(scoped_refptr<net::IOBuffer> buffer,
+ const net::CompletionCallback& callback,
+ int result);
+
+ const GetContentCallback get_content_callback_;
+ std::string data_;
+ scoped_ptr<net::URLFetcherFileWriter> file_writer_;
+ base::WeakPtrFactory<ResponseWriter> weak_ptr_factory_;
+
+ DISALLOW_COPY_AND_ASSIGN(ResponseWriter);
+};
+
+//============================ UrlFetchRequestBase ===========================
+
+// Base class for requests that are fetching URLs.
+class UrlFetchRequestBase : public AuthenticatedRequestInterface,
+ public net::URLFetcherDelegate {
+ public:
+ // AuthenticatedRequestInterface overrides.
+ virtual void Start(const std::string& access_token,
+ const std::string& custom_user_agent,
+ const ReAuthenticateCallback& callback) OVERRIDE;
+ virtual base::WeakPtr<AuthenticatedRequestInterface> GetWeakPtr() OVERRIDE;
+ virtual void Cancel() OVERRIDE;
+
+ protected:
+ explicit UrlFetchRequestBase(RequestSender* sender);
+ virtual ~UrlFetchRequestBase();
+
+ // Gets URL for the request.
+ virtual GURL GetURL() const = 0;
+
+ // Returns the request type. A derived class should override this method
+ // for a request type other than HTTP GET.
+ virtual net::URLFetcher::RequestType GetRequestType() const;
+
+ // Returns the extra HTTP headers for the request. A derived class should
+ // override this method to specify any extra headers needed for the request.
+ virtual std::vector<std::string> GetExtraRequestHeaders() const;
+
+ // Used by a derived class to add any content data to the request.
+ // Returns true if |upload_content_type| and |upload_content| are updated
+ // with the content type and data for the request.
+ // Note that this and GetContentFile() cannot be used together.
+ virtual bool GetContentData(std::string* upload_content_type,
+ std::string* upload_content);
+
+ // Used by a derived class to add content data which is the whole file or
+ // a part of the file at |local_file_path|.
+ // Returns true if all the arguments are updated for the content being
+ // uploaded.
+ // Note that this and GetContentData() cannot be used together.
+ virtual bool GetContentFile(base::FilePath* local_file_path,
+ int64* range_offset,
+ int64* range_length,
+ std::string* upload_content_type);
+
+ // Used by a derived class to set an output file path if they want to save
+ // the downloaded content to a file at a specific path.
+ // Sets |get_content_callback|, which is called when some part of the response
+ // is read.
+ virtual void GetOutputFilePath(base::FilePath* local_file_path,
+ GetContentCallback* get_content_callback);
+
+ // Invoked by OnURLFetchComplete when the request completes without an
+ // authentication error. Must be implemented by a derived class.
+ virtual void ProcessURLFetchResults(const net::URLFetcher* source) = 0;
+
+ // Invoked by this base class upon an authentication error or cancel by
+ // a user request. Must be implemented by a derived class.
+ virtual void RunCallbackOnPrematureFailure(GDataErrorCode code) = 0;
+
+ // Invoked from derived classes when ProcessURLFetchResults() is completed.
+ void OnProcessURLFetchResultsComplete();
+
+ // Returns an appropriate GDataErrorCode based on the HTTP response code and
+ // the status of the URLFetcher.
+ GDataErrorCode GetErrorCode();
+
+ // Returns true if called on the thread where the constructor was called.
+ bool CalledOnValidThread();
+
+ // Returns the writer which is used to save the response for the request.
+ ResponseWriter* response_writer() const { return response_writer_; }
+
+ // Returns the task runner that should be used for blocking tasks.
+ base::SequencedTaskRunner* blocking_task_runner() const;
+
+ private:
+ // URLFetcherDelegate overrides.
+ virtual void OnURLFetchComplete(const net::URLFetcher* source) OVERRIDE;
+
+ // AuthenticatedRequestInterface overrides.
+ virtual void OnAuthFailed(GDataErrorCode code) OVERRIDE;
+
+ ReAuthenticateCallback re_authenticate_callback_;
+ int re_authenticate_count_;
+ scoped_ptr<net::URLFetcher> url_fetcher_;
+ ResponseWriter* response_writer_; // Owned by |url_fetcher_|.
+ RequestSender* sender_;
+ GDataErrorCode error_code_;
+
+ base::ThreadChecker thread_checker_;
+
+ // Note: This should remain the last member so it'll be destroyed and
+ // invalidate its weak pointers before any other members are destroyed.
+ base::WeakPtrFactory<UrlFetchRequestBase> weak_ptr_factory_;
+
+ DISALLOW_COPY_AND_ASSIGN(UrlFetchRequestBase);
+};
+
+//============================ EntryActionRequest ============================
+
+// Callback type for requests that return only error status, like: Delete/Move.
+typedef base::Callback<void(GDataErrorCode error)> EntryActionCallback;
+
+// This class performs a simple action over a given entry (document/file).
+// It is meant to be used for requests that return no JSON blobs.
+class EntryActionRequest : public UrlFetchRequestBase {
+ public:
+ // |callback| is called when the request is finished either by success or by
+ // failure. It must not be null.
+ EntryActionRequest(RequestSender* sender,
+ const EntryActionCallback& callback);
+ virtual ~EntryActionRequest();
+
+ protected:
+ // Overridden from UrlFetchRequestBase.
+ virtual void ProcessURLFetchResults(const net::URLFetcher* source) OVERRIDE;
+ virtual void RunCallbackOnPrematureFailure(GDataErrorCode code) OVERRIDE;
+
+ private:
+ const EntryActionCallback callback_;
+
+ DISALLOW_COPY_AND_ASSIGN(EntryActionRequest);
+};
+
+//============================== GetDataRequest ==============================
+
+// Callback type for requests that returns JSON data.
+typedef base::Callback<void(GDataErrorCode error,
+ scoped_ptr<base::Value> json_data)> GetDataCallback;
+
+// This class performs the request for fetching and converting the fetched
+// content into a base::Value.
+class GetDataRequest : public UrlFetchRequestBase {
+ public:
+ // |callback| is called when the request finishes either by success or by
+ // failure. On success, a JSON Value object is passed. It must not be null.
+ GetDataRequest(RequestSender* sender, const GetDataCallback& callback);
+ virtual ~GetDataRequest();
+
+ protected:
+ // UrlFetchRequestBase overrides.
+ virtual void ProcessURLFetchResults(const net::URLFetcher* source) OVERRIDE;
+ virtual void RunCallbackOnPrematureFailure(
+ GDataErrorCode fetch_error_code) OVERRIDE;
+
+ private:
+ // Parses JSON response.
+ void ParseResponse(GDataErrorCode fetch_error_code, const std::string& data);
+
+ // Called when ParseJsonOnBlockingPool() is completed.
+ void OnDataParsed(GDataErrorCode fetch_error_code,
+ scoped_ptr<base::Value> value);
+
+ const GetDataCallback callback_;
+
+ // Note: This should remain the last member so it'll be destroyed and
+ // invalidate its weak pointers before any other members are destroyed.
+ base::WeakPtrFactory<GetDataRequest> weak_ptr_factory_;
+
+ DISALLOW_COPY_AND_ASSIGN(GetDataRequest);
+};
+
+
+//=========================== InitiateUploadRequestBase=======================
+
+// Callback type for DriveServiceInterface::InitiateUpload.
+typedef base::Callback<void(GDataErrorCode error,
+ const GURL& upload_url)> InitiateUploadCallback;
+
+// This class provides base implementation for performing the request for
+// initiating the upload of a file.
+// |callback| will be called with the obtained upload URL. The URL will be
+// used with requests for resuming the file uploading.
+//
+// Here's the flow of uploading:
+// 1) Get the upload URL with a class inheriting InitiateUploadRequestBase.
+// 2) Upload the first 1GB (see kUploadChunkSize in drive_uploader.cc)
+// of the target file to the upload URL
+// 3) If there is more data to upload, go to 2).
+//
+class InitiateUploadRequestBase : public UrlFetchRequestBase {
+ protected:
+ // |callback| will be called with the upload URL, where upload data is
+ // uploaded to with ResumeUploadRequestBase. It must not be null.
+ // |content_type| and |content_length| should be the attributes of the
+ // uploading file.
+ InitiateUploadRequestBase(RequestSender* sender,
+ const InitiateUploadCallback& callback,
+ const std::string& content_type,
+ int64 content_length);
+ virtual ~InitiateUploadRequestBase();
+
+ // UrlFetchRequestBase overrides.
+ virtual void ProcessURLFetchResults(const net::URLFetcher* source) OVERRIDE;
+ virtual void RunCallbackOnPrematureFailure(GDataErrorCode code) OVERRIDE;
+ virtual std::vector<std::string> GetExtraRequestHeaders() const OVERRIDE;
+
+ private:
+ const InitiateUploadCallback callback_;
+ const std::string content_type_;
+ const int64 content_length_;
+
+ DISALLOW_COPY_AND_ASSIGN(InitiateUploadRequestBase);
+};
+
+//========================== UploadRangeRequestBase ==========================
+
+// Struct for response to ResumeUpload and GetUploadStatus.
+struct UploadRangeResponse {
+ UploadRangeResponse();
+ UploadRangeResponse(GDataErrorCode code,
+ int64 start_position_received,
+ int64 end_position_received);
+ ~UploadRangeResponse();
+
+ GDataErrorCode code;
+ // The values of "Range" header returned from the server. The values are
+ // used to continue uploading more data. These are set to -1 if an upload
+ // is complete.
+ // |start_position_received| is inclusive and |end_position_received| is
+ // exclusive to follow the common C++ manner, although the response from
+ // the server has "Range" header in inclusive format at both sides.
+ int64 start_position_received;
+ int64 end_position_received;
+};
+
+// Base class for a URL fetch request expecting the response containing the
+// current uploading range. This class processes the response containing
+// "Range" header and invoke OnRangeRequestComplete.
+class UploadRangeRequestBase : public UrlFetchRequestBase {
+ protected:
+ // |upload_url| is the URL of where to upload the file to.
+ UploadRangeRequestBase(RequestSender* sender, const GURL& upload_url);
+ virtual ~UploadRangeRequestBase();
+
+ // UrlFetchRequestBase overrides.
+ virtual GURL GetURL() const OVERRIDE;
+ virtual net::URLFetcher::RequestType GetRequestType() const OVERRIDE;
+ virtual void ProcessURLFetchResults(const net::URLFetcher* source) OVERRIDE;
+ virtual void RunCallbackOnPrematureFailure(GDataErrorCode code) OVERRIDE;
+
+ // This method will be called when the request is done, regardless of
+ // whether it is succeeded or failed.
+ //
+ // 1) If there is more data to upload, |code| of |response| is set to
+ // HTTP_RESUME_INCOMPLETE, and positions are set appropriately. Also, |value|
+ // will be set to NULL.
+ // 2) If the upload is complete, |code| is set to HTTP_CREATED for a new file
+ // or HTTP_SUCCESS for an existing file. Positions are set to -1, and |value|
+ // is set to a parsed JSON value representing the uploaded file.
+ // 3) If a premature failure is found, |code| is set to a value representing
+ // the situation. Positions are set to 0, and |value| is set to NULL.
+ //
+ // See also the comments for UploadRangeResponse.
+ // Note: Subclasses should have responsibility to run some callback
+ // in this method to notify the finish status to its clients (or ignore it
+ // under its responsibility).
+ virtual void OnRangeRequestComplete(
+ const UploadRangeResponse& response, scoped_ptr<base::Value> value) = 0;
+
+ private:
+ // Called when ParseJson() is completed.
+ void OnDataParsed(GDataErrorCode code, scoped_ptr<base::Value> value);
+
+ const GURL upload_url_;
+
+ // Note: This should remain the last member so it'll be destroyed and
+ // invalidate its weak pointers before any other members are destroyed.
+ base::WeakPtrFactory<UploadRangeRequestBase> weak_ptr_factory_;
+
+ DISALLOW_COPY_AND_ASSIGN(UploadRangeRequestBase);
+};
+
+//========================== ResumeUploadRequestBase =========================
+
+// This class performs the request for resuming the upload of a file.
+// More specifically, this request uploads a chunk of data carried in |buf|
+// of ResumeUploadResponseBase. This class is designed to share the
+// implementation of upload resuming between GData WAPI and Drive API v2.
+// The subclasses should implement OnRangeRequestComplete inherited by
+// UploadRangeRequestBase, because the type of the response should be
+// different (although the format in the server response is JSON).
+class ResumeUploadRequestBase : public UploadRangeRequestBase {
+ protected:
+ // |start_position| is the start of range of contents currently stored in
+ // |buf|. |end_position| is the end of range of contents currently stared in
+ // |buf|. This is exclusive. For instance, if you are to upload the first
+ // 500 bytes of data, |start_position| is 0 and |end_position| is 500.
+ // |content_length| and |content_type| are the length and type of the
+ // file content to be uploaded respectively.
+ // |buf| holds current content to be uploaded.
+ // See also UploadRangeRequestBase's comment for remaining parameters
+ // meaning.
+ ResumeUploadRequestBase(RequestSender* sender,
+ const GURL& upload_location,
+ int64 start_position,
+ int64 end_position,
+ int64 content_length,
+ const std::string& content_type,
+ const base::FilePath& local_file_path);
+ virtual ~ResumeUploadRequestBase();
+
+ // UrlFetchRequestBase overrides.
+ virtual std::vector<std::string> GetExtraRequestHeaders() const OVERRIDE;
+ virtual bool GetContentFile(base::FilePath* local_file_path,
+ int64* range_offset,
+ int64* range_length,
+ std::string* upload_content_type) OVERRIDE;
+
+ private:
+ // The parameters for the request. See ResumeUploadParams for the details.
+ const int64 start_position_;
+ const int64 end_position_;
+ const int64 content_length_;
+ const std::string content_type_;
+ const base::FilePath local_file_path_;
+
+ DISALLOW_COPY_AND_ASSIGN(ResumeUploadRequestBase);
+};
+
+//======================== GetUploadStatusRequestBase ========================
+
+// This class performs the request for getting the current upload status
+// of a file.
+// This request calls OnRangeRequestComplete() with:
+// - HTTP_RESUME_INCOMPLETE and the range of previously uploaded data,
+// if a file has been partially uploaded. |value| is not used.
+// - HTTP_SUCCESS or HTTP_CREATED (up to the upload mode) and |value|
+// for the uploaded data, if a file has been completely uploaded.
+// See also UploadRangeRequestBase.
+class GetUploadStatusRequestBase : public UploadRangeRequestBase {
+ public:
+ // |content_length| is the whole data size to be uploaded.
+ // See also UploadRangeRequestBase's constructor comment for other
+ // parameters.
+ GetUploadStatusRequestBase(RequestSender* sender,
+ const GURL& upload_url,
+ int64 content_length);
+ virtual ~GetUploadStatusRequestBase();
+
+ protected:
+ // UrlFetchRequestBase overrides.
+ virtual std::vector<std::string> GetExtraRequestHeaders() const OVERRIDE;
+
+ private:
+ const int64 content_length_;
+
+ DISALLOW_COPY_AND_ASSIGN(GetUploadStatusRequestBase);
+};
+
+//============================ DownloadFileRequest ===========================
+
+// Callback type for receiving the completion of DownloadFileRequest.
+typedef base::Callback<void(GDataErrorCode error,
+ const base::FilePath& temp_file)>
+ DownloadActionCallback;
+
+// This is a base class for performing the request for downloading a file.
+class DownloadFileRequestBase : public UrlFetchRequestBase {
+ public:
+ // download_action_callback:
+ // This callback is called when the download is complete. Must not be null.
+ //
+ // get_content_callback:
+ // This callback is called when some part of the content is
+ // read. Used to read the download content progressively. May be null.
+ //
+ // progress_callback:
+ // This callback is called for periodically reporting the number of bytes
+ // downloaded so far. May be null.
+ //
+ // download_url:
+ // Specifies the target file to download.
+ //
+ // output_file_path:
+ // Specifies the file path to save the downloaded file.
+ //
+ DownloadFileRequestBase(
+ RequestSender* sender,
+ const DownloadActionCallback& download_action_callback,
+ const GetContentCallback& get_content_callback,
+ const ProgressCallback& progress_callback,
+ const GURL& download_url,
+ const base::FilePath& output_file_path);
+ virtual ~DownloadFileRequestBase();
+
+ protected:
+ // UrlFetchRequestBase overrides.
+ virtual GURL GetURL() const OVERRIDE;
+ virtual void GetOutputFilePath(
+ base::FilePath* local_file_path,
+ GetContentCallback* get_content_callback) OVERRIDE;
+ virtual void ProcessURLFetchResults(const net::URLFetcher* source) OVERRIDE;
+ virtual void RunCallbackOnPrematureFailure(GDataErrorCode code) OVERRIDE;
+
+ // net::URLFetcherDelegate overrides.
+ virtual void OnURLFetchDownloadProgress(const net::URLFetcher* source,
+ int64 current, int64 total) OVERRIDE;
+
+ private:
+ const DownloadActionCallback download_action_callback_;
+ const GetContentCallback get_content_callback_;
+ const ProgressCallback progress_callback_;
+ const GURL download_url_;
+ const base::FilePath output_file_path_;
+
+ DISALLOW_COPY_AND_ASSIGN(DownloadFileRequestBase);
+};
+
+} // namespace google_apis
+
+#endif // GOOGLE_APIS_DRIVE_BASE_REQUESTS_H_
diff --git a/chromium/google_apis/drive/base_requests_server_unittest.cc b/chromium/google_apis/drive/base_requests_server_unittest.cc
new file mode 100644
index 00000000000..bd54fcbdef8
--- /dev/null
+++ b/chromium/google_apis/drive/base_requests_server_unittest.cc
@@ -0,0 +1,131 @@
+// Copyright (c) 2013 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "google_apis/drive/base_requests.h"
+
+#include "base/bind.h"
+#include "base/file_util.h"
+#include "base/files/scoped_temp_dir.h"
+#include "base/memory/scoped_ptr.h"
+#include "base/message_loop/message_loop.h"
+#include "base/run_loop.h"
+#include "google_apis/drive/dummy_auth_service.h"
+#include "google_apis/drive/request_sender.h"
+#include "google_apis/drive/task_util.h"
+#include "google_apis/drive/test_util.h"
+#include "net/test/embedded_test_server/embedded_test_server.h"
+#include "net/test/embedded_test_server/http_request.h"
+#include "net/test/embedded_test_server/http_response.h"
+#include "net/url_request/url_request_test_util.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+namespace google_apis {
+
+namespace {
+
+const char kTestUserAgent[] = "test-user-agent";
+
+} // namespace
+
+class BaseRequestsServerTest : public testing::Test {
+ protected:
+ BaseRequestsServerTest() {
+ }
+
+ virtual void SetUp() OVERRIDE {
+ ASSERT_TRUE(temp_dir_.CreateUniqueTempDir());
+
+ request_context_getter_ = new net::TestURLRequestContextGetter(
+ message_loop_.message_loop_proxy());
+
+ request_sender_.reset(new RequestSender(
+ new DummyAuthService,
+ request_context_getter_.get(),
+ message_loop_.message_loop_proxy(),
+ kTestUserAgent));
+
+ ASSERT_TRUE(test_server_.InitializeAndWaitUntilReady());
+ test_server_.RegisterRequestHandler(
+ base::Bind(&test_util::HandleDownloadFileRequest,
+ test_server_.base_url(),
+ base::Unretained(&http_request_)));
+ }
+
+ // Returns a temporary file path suitable for storing the cache file.
+ base::FilePath GetTestCachedFilePath(const base::FilePath& file_name) {
+ return temp_dir_.path().Append(file_name);
+ }
+
+ base::MessageLoopForIO message_loop_; // Test server needs IO thread.
+ net::test_server::EmbeddedTestServer test_server_;
+ scoped_ptr<RequestSender> request_sender_;
+ scoped_refptr<net::TestURLRequestContextGetter> request_context_getter_;
+ base::ScopedTempDir temp_dir_;
+
+ // The incoming HTTP request is saved so tests can verify the request
+ // parameters like HTTP method (ex. some requests should use DELETE
+ // instead of GET).
+ net::test_server::HttpRequest http_request_;
+};
+
+TEST_F(BaseRequestsServerTest, DownloadFileRequest_ValidFile) {
+ GDataErrorCode result_code = GDATA_OTHER_ERROR;
+ base::FilePath temp_file;
+ {
+ base::RunLoop run_loop;
+ DownloadFileRequestBase* request = new DownloadFileRequestBase(
+ request_sender_.get(),
+ test_util::CreateQuitCallback(
+ &run_loop,
+ test_util::CreateCopyResultCallback(&result_code, &temp_file)),
+ GetContentCallback(),
+ ProgressCallback(),
+ test_server_.GetURL("/files/gdata/testfile.txt"),
+ GetTestCachedFilePath(
+ base::FilePath::FromUTF8Unsafe("cached_testfile.txt")));
+ request_sender_->StartRequestWithRetry(request);
+ run_loop.Run();
+ }
+
+ std::string contents;
+ base::ReadFileToString(temp_file, &contents);
+ base::DeleteFile(temp_file, false);
+
+ EXPECT_EQ(HTTP_SUCCESS, result_code);
+ EXPECT_EQ(net::test_server::METHOD_GET, http_request_.method);
+ EXPECT_EQ("/files/gdata/testfile.txt", http_request_.relative_url);
+
+ const base::FilePath expected_path =
+ test_util::GetTestFilePath("gdata/testfile.txt");
+ std::string expected_contents;
+ base::ReadFileToString(expected_path, &expected_contents);
+ EXPECT_EQ(expected_contents, contents);
+}
+
+TEST_F(BaseRequestsServerTest, DownloadFileRequest_NonExistentFile) {
+ GDataErrorCode result_code = GDATA_OTHER_ERROR;
+ base::FilePath temp_file;
+ {
+ base::RunLoop run_loop;
+ DownloadFileRequestBase* request = new DownloadFileRequestBase(
+ request_sender_.get(),
+ test_util::CreateQuitCallback(
+ &run_loop,
+ test_util::CreateCopyResultCallback(&result_code, &temp_file)),
+ GetContentCallback(),
+ ProgressCallback(),
+ test_server_.GetURL("/files/gdata/no-such-file.txt"),
+ GetTestCachedFilePath(
+ base::FilePath::FromUTF8Unsafe("cache_no-such-file.txt")));
+ request_sender_->StartRequestWithRetry(request);
+ run_loop.Run();
+ }
+ EXPECT_EQ(HTTP_NOT_FOUND, result_code);
+ EXPECT_EQ(net::test_server::METHOD_GET, http_request_.method);
+ EXPECT_EQ("/files/gdata/no-such-file.txt",
+ http_request_.relative_url);
+ // Do not verify the not found message.
+}
+
+} // namespace google_apis
diff --git a/chromium/google_apis/drive/base_requests_unittest.cc b/chromium/google_apis/drive/base_requests_unittest.cc
new file mode 100644
index 00000000000..3032d2abb37
--- /dev/null
+++ b/chromium/google_apis/drive/base_requests_unittest.cc
@@ -0,0 +1,204 @@
+// Copyright (c) 2012 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "google_apis/drive/base_requests.h"
+
+#include "base/bind.h"
+#include "base/memory/scoped_ptr.h"
+#include "base/message_loop/message_loop.h"
+#include "base/run_loop.h"
+#include "base/values.h"
+#include "google_apis/drive/dummy_auth_service.h"
+#include "google_apis/drive/request_sender.h"
+#include "google_apis/drive/test_util.h"
+#include "net/test/embedded_test_server/embedded_test_server.h"
+#include "net/test/embedded_test_server/http_request.h"
+#include "net/test/embedded_test_server/http_response.h"
+#include "net/url_request/url_request_test_util.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+namespace google_apis {
+
+namespace {
+
+const char kValidJsonString[] = "{ \"test\": 123 }";
+const char kInvalidJsonString[] = "$$$";
+
+class FakeUrlFetchRequest : public UrlFetchRequestBase {
+ public:
+ explicit FakeUrlFetchRequest(RequestSender* sender,
+ const EntryActionCallback& callback,
+ const GURL& url)
+ : UrlFetchRequestBase(sender),
+ callback_(callback),
+ url_(url) {
+ }
+
+ virtual ~FakeUrlFetchRequest() {
+ }
+
+ protected:
+ virtual GURL GetURL() const OVERRIDE { return url_; }
+ virtual void ProcessURLFetchResults(const net::URLFetcher* source) OVERRIDE {
+ callback_.Run(GetErrorCode());
+ }
+ virtual void RunCallbackOnPrematureFailure(GDataErrorCode code) OVERRIDE {
+ callback_.Run(code);
+ }
+
+ EntryActionCallback callback_;
+ GURL url_;
+};
+
+class FakeGetDataRequest : public GetDataRequest {
+ public:
+ explicit FakeGetDataRequest(RequestSender* sender,
+ const GetDataCallback& callback,
+ const GURL& url)
+ : GetDataRequest(sender, callback),
+ url_(url) {
+ }
+
+ virtual ~FakeGetDataRequest() {
+ }
+
+ protected:
+ virtual GURL GetURL() const OVERRIDE { return url_; }
+
+ GURL url_;
+};
+
+} // namespace
+
+class BaseRequestsTest : public testing::Test {
+ public:
+ BaseRequestsTest() : response_code_(net::HTTP_OK) {}
+
+ virtual void SetUp() OVERRIDE {
+ request_context_getter_ = new net::TestURLRequestContextGetter(
+ message_loop_.message_loop_proxy());
+
+ sender_.reset(new RequestSender(new DummyAuthService,
+ request_context_getter_.get(),
+ message_loop_.message_loop_proxy(),
+ std::string() /* custom user agent */));
+
+ ASSERT_TRUE(test_server_.InitializeAndWaitUntilReady());
+ test_server_.RegisterRequestHandler(
+ base::Bind(&BaseRequestsTest::HandleRequest, base::Unretained(this)));
+ }
+
+ scoped_ptr<net::test_server::HttpResponse> HandleRequest(
+ const net::test_server::HttpRequest& request) {
+ scoped_ptr<net::test_server::BasicHttpResponse> response(
+ new net::test_server::BasicHttpResponse);
+ response->set_code(response_code_);
+ response->set_content(response_body_);
+ response->set_content_type("application/json");
+ return response.PassAs<net::test_server::HttpResponse>();
+ }
+
+ base::MessageLoopForIO message_loop_;
+ scoped_refptr<net::TestURLRequestContextGetter> request_context_getter_;
+ scoped_ptr<RequestSender> sender_;
+ net::test_server::EmbeddedTestServer test_server_;
+
+ net::HttpStatusCode response_code_;
+ std::string response_body_;
+};
+
+TEST_F(BaseRequestsTest, ParseValidJson) {
+ scoped_ptr<base::Value> json;
+ ParseJson(message_loop_.message_loop_proxy(),
+ kValidJsonString,
+ base::Bind(test_util::CreateCopyResultCallback(&json)));
+ base::RunLoop().RunUntilIdle();
+
+ DictionaryValue* root_dict = NULL;
+ ASSERT_TRUE(json);
+ ASSERT_TRUE(json->GetAsDictionary(&root_dict));
+
+ int int_value = 0;
+ ASSERT_TRUE(root_dict->GetInteger("test", &int_value));
+ EXPECT_EQ(123, int_value);
+}
+
+TEST_F(BaseRequestsTest, ParseInvalidJson) {
+ // Initialize with a valid pointer to verify that null is indeed assigned.
+ scoped_ptr<base::Value> json(base::Value::CreateNullValue());
+ ParseJson(message_loop_.message_loop_proxy(),
+ kInvalidJsonString,
+ base::Bind(test_util::CreateCopyResultCallback(&json)));
+ base::RunLoop().RunUntilIdle();
+
+ EXPECT_FALSE(json);
+}
+
+TEST_F(BaseRequestsTest, UrlFetchRequestBaseResponseCodeOverride) {
+ response_code_ = net::HTTP_FORBIDDEN;
+ response_body_ =
+ "{\"error\": {\n"
+ " \"errors\": [\n"
+ " {\n"
+ " \"domain\": \"usageLimits\",\n"
+ " \"reason\": \"rateLimitExceeded\",\n"
+ " \"message\": \"Rate Limit Exceeded\"\n"
+ " }\n"
+ " ],\n"
+ " \"code\": 403,\n"
+ " \"message\": \"Rate Limit Exceeded\"\n"
+ " }\n"
+ "}\n";
+
+ GDataErrorCode error = GDATA_OTHER_ERROR;
+ base::RunLoop run_loop;
+ sender_->StartRequestWithRetry(
+ new FakeUrlFetchRequest(
+ sender_.get(),
+ test_util::CreateQuitCallback(
+ &run_loop, test_util::CreateCopyResultCallback(&error)),
+ test_server_.base_url()));
+ run_loop.Run();
+
+ // HTTP_FORBIDDEN (403) is overridden by the error reason.
+ EXPECT_EQ(HTTP_SERVICE_UNAVAILABLE, error);
+}
+
+TEST_F(BaseRequestsTest, GetDataRequestParseValidResponse) {
+ response_body_ = kValidJsonString;
+
+ GDataErrorCode error = GDATA_OTHER_ERROR;
+ scoped_ptr<base::Value> value;
+ base::RunLoop run_loop;
+ sender_->StartRequestWithRetry(
+ new FakeGetDataRequest(
+ sender_.get(),
+ test_util::CreateQuitCallback(
+ &run_loop, test_util::CreateCopyResultCallback(&error, &value)),
+ test_server_.base_url()));
+ run_loop.Run();
+
+ EXPECT_EQ(HTTP_SUCCESS, error);
+ EXPECT_TRUE(value);
+}
+
+TEST_F(BaseRequestsTest, GetDataRequestParseInvalidResponse) {
+ response_body_ = kInvalidJsonString;
+
+ GDataErrorCode error = GDATA_OTHER_ERROR;
+ scoped_ptr<base::Value> value;
+ base::RunLoop run_loop;
+ sender_->StartRequestWithRetry(
+ new FakeGetDataRequest(
+ sender_.get(),
+ test_util::CreateQuitCallback(
+ &run_loop, test_util::CreateCopyResultCallback(&error, &value)),
+ test_server_.base_url()));
+ run_loop.Run();
+
+ EXPECT_EQ(GDATA_PARSE_ERROR, error);
+ EXPECT_FALSE(value);
+}
+
+} // namespace google_apis
diff --git a/chromium/google_apis/drive/drive_api_parser.cc b/chromium/google_apis/drive/drive_api_parser.cc
new file mode 100644
index 00000000000..4b05e8b5844
--- /dev/null
+++ b/chromium/google_apis/drive/drive_api_parser.cc
@@ -0,0 +1,729 @@
+// Copyright (c) 2012 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "google_apis/drive/drive_api_parser.h"
+
+#include <algorithm>
+
+#include "base/basictypes.h"
+#include "base/files/file_path.h"
+#include "base/json/json_value_converter.h"
+#include "base/memory/scoped_ptr.h"
+#include "base/strings/string_number_conversions.h"
+#include "base/strings/string_piece.h"
+#include "base/strings/string_util.h"
+#include "base/values.h"
+#include "google_apis/drive/time_util.h"
+
+using base::Value;
+using base::DictionaryValue;
+using base::ListValue;
+
+namespace google_apis {
+
+namespace {
+
+bool CreateFileResourceFromValue(const base::Value* value,
+ scoped_ptr<FileResource>* file) {
+ *file = FileResource::CreateFrom(*value);
+ return !!*file;
+}
+
+// Converts |url_string| to |result|. Always returns true to be used
+// for JSONValueConverter::RegisterCustomField method.
+// TODO(mukai): make it return false in case of invalid |url_string|.
+bool GetGURLFromString(const base::StringPiece& url_string, GURL* result) {
+ *result = GURL(url_string.as_string());
+ return true;
+}
+
+// Converts |value| to |result|. The key of |value| is app_id, and its value
+// is URL to open the resource on the web app.
+bool GetOpenWithLinksFromDictionaryValue(
+ const base::Value* value,
+ std::vector<FileResource::OpenWithLink>* result) {
+ DCHECK(value);
+ DCHECK(result);
+
+ const base::DictionaryValue* dictionary_value;
+ if (!value->GetAsDictionary(&dictionary_value))
+ return false;
+
+ result->reserve(dictionary_value->size());
+ for (DictionaryValue::Iterator iter(*dictionary_value);
+ !iter.IsAtEnd(); iter.Advance()) {
+ std::string string_value;
+ if (!iter.value().GetAsString(&string_value))
+ return false;
+
+ FileResource::OpenWithLink open_with_link;
+ open_with_link.app_id = iter.key();
+ open_with_link.open_url = GURL(string_value);
+ result->push_back(open_with_link);
+ }
+
+ return true;
+}
+
+// Drive v2 API JSON names.
+
+// Definition order follows the order of documentation in
+// https://developers.google.com/drive/v2/reference/
+
+// Common
+const char kKind[] = "kind";
+const char kId[] = "id";
+const char kETag[] = "etag";
+const char kSelfLink[] = "selfLink";
+const char kItems[] = "items";
+const char kLargestChangeId[] = "largestChangeId";
+
+// About Resource
+// https://developers.google.com/drive/v2/reference/about
+const char kAboutKind[] = "drive#about";
+const char kQuotaBytesTotal[] = "quotaBytesTotal";
+const char kQuotaBytesUsed[] = "quotaBytesUsed";
+const char kRootFolderId[] = "rootFolderId";
+
+// App Icon
+// https://developers.google.com/drive/v2/reference/apps
+const char kCategory[] = "category";
+const char kSize[] = "size";
+const char kIconUrl[] = "iconUrl";
+
+// Apps Resource
+// https://developers.google.com/drive/v2/reference/apps
+const char kAppKind[] = "drive#app";
+const char kName[] = "name";
+const char kObjectType[] = "objectType";
+const char kSupportsCreate[] = "supportsCreate";
+const char kSupportsImport[] = "supportsImport";
+const char kInstalled[] = "installed";
+const char kAuthorized[] = "authorized";
+const char kProductUrl[] = "productUrl";
+const char kPrimaryMimeTypes[] = "primaryMimeTypes";
+const char kSecondaryMimeTypes[] = "secondaryMimeTypes";
+const char kPrimaryFileExtensions[] = "primaryFileExtensions";
+const char kSecondaryFileExtensions[] = "secondaryFileExtensions";
+const char kIcons[] = "icons";
+
+// Apps List
+// https://developers.google.com/drive/v2/reference/apps/list
+const char kAppListKind[] = "drive#appList";
+
+// Parent Resource
+// https://developers.google.com/drive/v2/reference/parents
+const char kParentReferenceKind[] = "drive#parentReference";
+const char kParentLink[] = "parentLink";
+const char kIsRoot[] = "isRoot";
+
+// File Resource
+// https://developers.google.com/drive/v2/reference/files
+const char kFileKind[] = "drive#file";
+const char kTitle[] = "title";
+const char kMimeType[] = "mimeType";
+const char kCreatedDate[] = "createdDate";
+const char kModifiedDate[] = "modifiedDate";
+const char kModifiedByMeDate[] = "modifiedByMeDate";
+const char kLastViewedByMeDate[] = "lastViewedByMeDate";
+const char kSharedWithMeDate[] = "sharedWithMeDate";
+const char kDownloadUrl[] = "downloadUrl";
+const char kFileExtension[] = "fileExtension";
+const char kMd5Checksum[] = "md5Checksum";
+const char kFileSize[] = "fileSize";
+const char kAlternateLink[] = "alternateLink";
+const char kEmbedLink[] = "embedLink";
+const char kParents[] = "parents";
+const char kThumbnailLink[] = "thumbnailLink";
+const char kWebContentLink[] = "webContentLink";
+const char kOpenWithLinks[] = "openWithLinks";
+const char kLabels[] = "labels";
+const char kImageMediaMetadata[] = "imageMediaMetadata";
+const char kShared[] = "shared";
+// These 5 flags are defined under |labels|.
+const char kLabelStarred[] = "starred";
+const char kLabelHidden[] = "hidden";
+const char kLabelTrashed[] = "trashed";
+const char kLabelRestricted[] = "restricted";
+const char kLabelViewed[] = "viewed";
+// These 3 flags are defined under |imageMediaMetadata|.
+const char kImageMediaMetadataWidth[] = "width";
+const char kImageMediaMetadataHeight[] = "height";
+const char kImageMediaMetadataRotation[] = "rotation";
+
+const char kDriveFolderMimeType[] = "application/vnd.google-apps.folder";
+
+// Files List
+// https://developers.google.com/drive/v2/reference/files/list
+const char kFileListKind[] = "drive#fileList";
+const char kNextPageToken[] = "nextPageToken";
+const char kNextLink[] = "nextLink";
+
+// Change Resource
+// https://developers.google.com/drive/v2/reference/changes
+const char kChangeKind[] = "drive#change";
+const char kFileId[] = "fileId";
+const char kDeleted[] = "deleted";
+const char kFile[] = "file";
+
+// Changes List
+// https://developers.google.com/drive/v2/reference/changes/list
+const char kChangeListKind[] = "drive#changeList";
+
+// Maps category name to enum IconCategory.
+struct AppIconCategoryMap {
+ DriveAppIcon::IconCategory category;
+ const char* category_name;
+};
+
+const AppIconCategoryMap kAppIconCategoryMap[] = {
+ { DriveAppIcon::DOCUMENT, "document" },
+ { DriveAppIcon::APPLICATION, "application" },
+ { DriveAppIcon::SHARED_DOCUMENT, "documentShared" },
+};
+
+// Checks if the JSON is expected kind. In Drive API, JSON data structure has
+// |kind| property which denotes the type of the structure (e.g. "drive#file").
+bool IsResourceKindExpected(const base::Value& value,
+ const std::string& expected_kind) {
+ const base::DictionaryValue* as_dict = NULL;
+ std::string kind;
+ return value.GetAsDictionary(&as_dict) &&
+ as_dict->HasKey(kKind) &&
+ as_dict->GetString(kKind, &kind) &&
+ kind == expected_kind;
+}
+
+} // namespace
+
+////////////////////////////////////////////////////////////////////////////////
+// AboutResource implementation
+
+AboutResource::AboutResource()
+ : largest_change_id_(0),
+ quota_bytes_total_(0),
+ quota_bytes_used_(0) {}
+
+AboutResource::~AboutResource() {}
+
+// static
+scoped_ptr<AboutResource> AboutResource::CreateFrom(const base::Value& value) {
+ scoped_ptr<AboutResource> resource(new AboutResource());
+ if (!IsResourceKindExpected(value, kAboutKind) || !resource->Parse(value)) {
+ LOG(ERROR) << "Unable to create: Invalid About resource JSON!";
+ return scoped_ptr<AboutResource>();
+ }
+ return resource.Pass();
+}
+
+// static
+void AboutResource::RegisterJSONConverter(
+ base::JSONValueConverter<AboutResource>* converter) {
+ converter->RegisterCustomField<int64>(kLargestChangeId,
+ &AboutResource::largest_change_id_,
+ &base::StringToInt64);
+ converter->RegisterCustomField<int64>(kQuotaBytesTotal,
+ &AboutResource::quota_bytes_total_,
+ &base::StringToInt64);
+ converter->RegisterCustomField<int64>(kQuotaBytesUsed,
+ &AboutResource::quota_bytes_used_,
+ &base::StringToInt64);
+ converter->RegisterStringField(kRootFolderId,
+ &AboutResource::root_folder_id_);
+}
+
+bool AboutResource::Parse(const base::Value& value) {
+ base::JSONValueConverter<AboutResource> converter;
+ if (!converter.Convert(value, this)) {
+ LOG(ERROR) << "Unable to parse: Invalid About resource JSON!";
+ return false;
+ }
+ return true;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// DriveAppIcon implementation
+
+DriveAppIcon::DriveAppIcon() : category_(UNKNOWN), icon_side_length_(0) {}
+
+DriveAppIcon::~DriveAppIcon() {}
+
+// static
+void DriveAppIcon::RegisterJSONConverter(
+ base::JSONValueConverter<DriveAppIcon>* converter) {
+ converter->RegisterCustomField<IconCategory>(
+ kCategory,
+ &DriveAppIcon::category_,
+ &DriveAppIcon::GetIconCategory);
+ converter->RegisterIntField(kSize, &DriveAppIcon::icon_side_length_);
+ converter->RegisterCustomField<GURL>(kIconUrl,
+ &DriveAppIcon::icon_url_,
+ GetGURLFromString);
+}
+
+// static
+scoped_ptr<DriveAppIcon> DriveAppIcon::CreateFrom(const base::Value& value) {
+ scoped_ptr<DriveAppIcon> resource(new DriveAppIcon());
+ if (!resource->Parse(value)) {
+ LOG(ERROR) << "Unable to create: Invalid DriveAppIcon JSON!";
+ return scoped_ptr<DriveAppIcon>();
+ }
+ return resource.Pass();
+}
+
+bool DriveAppIcon::Parse(const base::Value& value) {
+ base::JSONValueConverter<DriveAppIcon> converter;
+ if (!converter.Convert(value, this)) {
+ LOG(ERROR) << "Unable to parse: Invalid DriveAppIcon";
+ return false;
+ }
+ return true;
+}
+
+// static
+bool DriveAppIcon::GetIconCategory(const base::StringPiece& category,
+ DriveAppIcon::IconCategory* result) {
+ for (size_t i = 0; i < arraysize(kAppIconCategoryMap); i++) {
+ if (category == kAppIconCategoryMap[i].category_name) {
+ *result = kAppIconCategoryMap[i].category;
+ return true;
+ }
+ }
+ DVLOG(1) << "Unknown icon category " << category;
+ return false;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// AppResource implementation
+
+AppResource::AppResource()
+ : supports_create_(false),
+ supports_import_(false),
+ installed_(false),
+ authorized_(false) {
+}
+
+AppResource::~AppResource() {}
+
+// static
+void AppResource::RegisterJSONConverter(
+ base::JSONValueConverter<AppResource>* converter) {
+ converter->RegisterStringField(kId, &AppResource::application_id_);
+ converter->RegisterStringField(kName, &AppResource::name_);
+ converter->RegisterStringField(kObjectType, &AppResource::object_type_);
+ converter->RegisterBoolField(kSupportsCreate, &AppResource::supports_create_);
+ converter->RegisterBoolField(kSupportsImport, &AppResource::supports_import_);
+ converter->RegisterBoolField(kInstalled, &AppResource::installed_);
+ converter->RegisterBoolField(kAuthorized, &AppResource::authorized_);
+ converter->RegisterCustomField<GURL>(kProductUrl,
+ &AppResource::product_url_,
+ GetGURLFromString);
+ converter->RegisterRepeatedString(kPrimaryMimeTypes,
+ &AppResource::primary_mimetypes_);
+ converter->RegisterRepeatedString(kSecondaryMimeTypes,
+ &AppResource::secondary_mimetypes_);
+ converter->RegisterRepeatedString(kPrimaryFileExtensions,
+ &AppResource::primary_file_extensions_);
+ converter->RegisterRepeatedString(kSecondaryFileExtensions,
+ &AppResource::secondary_file_extensions_);
+ converter->RegisterRepeatedMessage(kIcons, &AppResource::icons_);
+}
+
+// static
+scoped_ptr<AppResource> AppResource::CreateFrom(const base::Value& value) {
+ scoped_ptr<AppResource> resource(new AppResource());
+ if (!IsResourceKindExpected(value, kAppKind) || !resource->Parse(value)) {
+ LOG(ERROR) << "Unable to create: Invalid AppResource JSON!";
+ return scoped_ptr<AppResource>();
+ }
+ return resource.Pass();
+}
+
+bool AppResource::Parse(const base::Value& value) {
+ base::JSONValueConverter<AppResource> converter;
+ if (!converter.Convert(value, this)) {
+ LOG(ERROR) << "Unable to parse: Invalid AppResource";
+ return false;
+ }
+ return true;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// AppList implementation
+
+AppList::AppList() {}
+
+AppList::~AppList() {}
+
+// static
+void AppList::RegisterJSONConverter(
+ base::JSONValueConverter<AppList>* converter) {
+ converter->RegisterStringField(kETag, &AppList::etag_);
+ converter->RegisterRepeatedMessage<AppResource>(kItems,
+ &AppList::items_);
+}
+
+// static
+scoped_ptr<AppList> AppList::CreateFrom(const base::Value& value) {
+ scoped_ptr<AppList> resource(new AppList());
+ if (!IsResourceKindExpected(value, kAppListKind) || !resource->Parse(value)) {
+ LOG(ERROR) << "Unable to create: Invalid AppList JSON!";
+ return scoped_ptr<AppList>();
+ }
+ return resource.Pass();
+}
+
+bool AppList::Parse(const base::Value& value) {
+ base::JSONValueConverter<AppList> converter;
+ if (!converter.Convert(value, this)) {
+ LOG(ERROR) << "Unable to parse: Invalid AppList";
+ return false;
+ }
+ return true;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// ParentReference implementation
+
+ParentReference::ParentReference() : is_root_(false) {}
+
+ParentReference::~ParentReference() {}
+
+// static
+void ParentReference::RegisterJSONConverter(
+ base::JSONValueConverter<ParentReference>* converter) {
+ converter->RegisterStringField(kId, &ParentReference::file_id_);
+ converter->RegisterCustomField<GURL>(kParentLink,
+ &ParentReference::parent_link_,
+ GetGURLFromString);
+ converter->RegisterBoolField(kIsRoot, &ParentReference::is_root_);
+}
+
+// static
+scoped_ptr<ParentReference>
+ParentReference::CreateFrom(const base::Value& value) {
+ scoped_ptr<ParentReference> reference(new ParentReference());
+ if (!IsResourceKindExpected(value, kParentReferenceKind) ||
+ !reference->Parse(value)) {
+ LOG(ERROR) << "Unable to create: Invalid ParentRefernce JSON!";
+ return scoped_ptr<ParentReference>();
+ }
+ return reference.Pass();
+}
+
+bool ParentReference::Parse(const base::Value& value) {
+ base::JSONValueConverter<ParentReference> converter;
+ if (!converter.Convert(value, this)) {
+ LOG(ERROR) << "Unable to parse: Invalid ParentReference";
+ return false;
+ }
+ return true;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// FileResource implementation
+
+FileResource::FileResource() : shared_(false), file_size_(0) {}
+
+FileResource::~FileResource() {}
+
+// static
+void FileResource::RegisterJSONConverter(
+ base::JSONValueConverter<FileResource>* converter) {
+ converter->RegisterStringField(kId, &FileResource::file_id_);
+ converter->RegisterStringField(kETag, &FileResource::etag_);
+ converter->RegisterCustomField<GURL>(kSelfLink,
+ &FileResource::self_link_,
+ GetGURLFromString);
+ converter->RegisterStringField(kTitle, &FileResource::title_);
+ converter->RegisterStringField(kMimeType, &FileResource::mime_type_);
+ converter->RegisterNestedField(kLabels, &FileResource::labels_);
+ converter->RegisterNestedField(kImageMediaMetadata,
+ &FileResource::image_media_metadata_);
+ converter->RegisterCustomField<base::Time>(
+ kCreatedDate,
+ &FileResource::created_date_,
+ &util::GetTimeFromString);
+ converter->RegisterCustomField<base::Time>(
+ kModifiedDate,
+ &FileResource::modified_date_,
+ &util::GetTimeFromString);
+ converter->RegisterCustomField<base::Time>(
+ kModifiedByMeDate,
+ &FileResource::modified_by_me_date_,
+ &util::GetTimeFromString);
+ converter->RegisterCustomField<base::Time>(
+ kLastViewedByMeDate,
+ &FileResource::last_viewed_by_me_date_,
+ &util::GetTimeFromString);
+ converter->RegisterCustomField<base::Time>(
+ kSharedWithMeDate,
+ &FileResource::shared_with_me_date_,
+ &util::GetTimeFromString);
+ converter->RegisterBoolField(kShared, &FileResource::shared_);
+ converter->RegisterCustomField<GURL>(kDownloadUrl,
+ &FileResource::download_url_,
+ GetGURLFromString);
+ converter->RegisterStringField(kFileExtension,
+ &FileResource::file_extension_);
+ converter->RegisterStringField(kMd5Checksum, &FileResource::md5_checksum_);
+ converter->RegisterCustomField<int64>(kFileSize,
+ &FileResource::file_size_,
+ &base::StringToInt64);
+ converter->RegisterCustomField<GURL>(kAlternateLink,
+ &FileResource::alternate_link_,
+ GetGURLFromString);
+ converter->RegisterCustomField<GURL>(kEmbedLink,
+ &FileResource::embed_link_,
+ GetGURLFromString);
+ converter->RegisterRepeatedMessage<ParentReference>(kParents,
+ &FileResource::parents_);
+ converter->RegisterCustomField<GURL>(kThumbnailLink,
+ &FileResource::thumbnail_link_,
+ GetGURLFromString);
+ converter->RegisterCustomField<GURL>(kWebContentLink,
+ &FileResource::web_content_link_,
+ GetGURLFromString);
+ converter->RegisterCustomValueField<std::vector<OpenWithLink> >(
+ kOpenWithLinks,
+ &FileResource::open_with_links_,
+ GetOpenWithLinksFromDictionaryValue);
+}
+
+// static
+scoped_ptr<FileResource> FileResource::CreateFrom(const base::Value& value) {
+ scoped_ptr<FileResource> resource(new FileResource());
+ if (!IsResourceKindExpected(value, kFileKind) || !resource->Parse(value)) {
+ LOG(ERROR) << "Unable to create: Invalid FileResource JSON!";
+ return scoped_ptr<FileResource>();
+ }
+ return resource.Pass();
+}
+
+bool FileResource::IsDirectory() const {
+ return mime_type_ == kDriveFolderMimeType;
+}
+
+bool FileResource::Parse(const base::Value& value) {
+ base::JSONValueConverter<FileResource> converter;
+ if (!converter.Convert(value, this)) {
+ LOG(ERROR) << "Unable to parse: Invalid FileResource";
+ return false;
+ }
+ return true;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// FileList implementation
+
+FileList::FileList() {}
+
+FileList::~FileList() {}
+
+// static
+void FileList::RegisterJSONConverter(
+ base::JSONValueConverter<FileList>* converter) {
+ converter->RegisterStringField(kETag, &FileList::etag_);
+ converter->RegisterStringField(kNextPageToken, &FileList::next_page_token_);
+ converter->RegisterCustomField<GURL>(kNextLink,
+ &FileList::next_link_,
+ GetGURLFromString);
+ converter->RegisterRepeatedMessage<FileResource>(kItems,
+ &FileList::items_);
+}
+
+// static
+bool FileList::HasFileListKind(const base::Value& value) {
+ return IsResourceKindExpected(value, kFileListKind);
+}
+
+// static
+scoped_ptr<FileList> FileList::CreateFrom(const base::Value& value) {
+ scoped_ptr<FileList> resource(new FileList());
+ if (!HasFileListKind(value) || !resource->Parse(value)) {
+ LOG(ERROR) << "Unable to create: Invalid FileList JSON!";
+ return scoped_ptr<FileList>();
+ }
+ return resource.Pass();
+}
+
+bool FileList::Parse(const base::Value& value) {
+ base::JSONValueConverter<FileList> converter;
+ if (!converter.Convert(value, this)) {
+ LOG(ERROR) << "Unable to parse: Invalid FileList";
+ return false;
+ }
+ return true;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// ChangeResource implementation
+
+ChangeResource::ChangeResource() : change_id_(0), deleted_(false) {}
+
+ChangeResource::~ChangeResource() {}
+
+// static
+void ChangeResource::RegisterJSONConverter(
+ base::JSONValueConverter<ChangeResource>* converter) {
+ converter->RegisterCustomField<int64>(kId,
+ &ChangeResource::change_id_,
+ &base::StringToInt64);
+ converter->RegisterStringField(kFileId, &ChangeResource::file_id_);
+ converter->RegisterBoolField(kDeleted, &ChangeResource::deleted_);
+ converter->RegisterCustomValueField(kFile, &ChangeResource::file_,
+ &CreateFileResourceFromValue);
+}
+
+// static
+scoped_ptr<ChangeResource>
+ChangeResource::CreateFrom(const base::Value& value) {
+ scoped_ptr<ChangeResource> resource(new ChangeResource());
+ if (!IsResourceKindExpected(value, kChangeKind) || !resource->Parse(value)) {
+ LOG(ERROR) << "Unable to create: Invalid ChangeResource JSON!";
+ return scoped_ptr<ChangeResource>();
+ }
+ return resource.Pass();
+}
+
+bool ChangeResource::Parse(const base::Value& value) {
+ base::JSONValueConverter<ChangeResource> converter;
+ if (!converter.Convert(value, this)) {
+ LOG(ERROR) << "Unable to parse: Invalid ChangeResource";
+ return false;
+ }
+ return true;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// ChangeList implementation
+
+ChangeList::ChangeList() : largest_change_id_(0) {}
+
+ChangeList::~ChangeList() {}
+
+// static
+void ChangeList::RegisterJSONConverter(
+ base::JSONValueConverter<ChangeList>* converter) {
+ converter->RegisterStringField(kETag, &ChangeList::etag_);
+ converter->RegisterStringField(kNextPageToken, &ChangeList::next_page_token_);
+ converter->RegisterCustomField<GURL>(kNextLink,
+ &ChangeList::next_link_,
+ GetGURLFromString);
+ converter->RegisterCustomField<int64>(kLargestChangeId,
+ &ChangeList::largest_change_id_,
+ &base::StringToInt64);
+ converter->RegisterRepeatedMessage<ChangeResource>(kItems,
+ &ChangeList::items_);
+}
+
+// static
+bool ChangeList::HasChangeListKind(const base::Value& value) {
+ return IsResourceKindExpected(value, kChangeListKind);
+}
+
+// static
+scoped_ptr<ChangeList> ChangeList::CreateFrom(const base::Value& value) {
+ scoped_ptr<ChangeList> resource(new ChangeList());
+ if (!HasChangeListKind(value) || !resource->Parse(value)) {
+ LOG(ERROR) << "Unable to create: Invalid ChangeList JSON!";
+ return scoped_ptr<ChangeList>();
+ }
+ return resource.Pass();
+}
+
+bool ChangeList::Parse(const base::Value& value) {
+ base::JSONValueConverter<ChangeList> converter;
+ if (!converter.Convert(value, this)) {
+ LOG(ERROR) << "Unable to parse: Invalid ChangeList";
+ return false;
+ }
+ return true;
+}
+
+
+////////////////////////////////////////////////////////////////////////////////
+// FileLabels implementation
+
+FileLabels::FileLabels()
+ : starred_(false),
+ hidden_(false),
+ trashed_(false),
+ restricted_(false),
+ viewed_(false) {}
+
+FileLabels::~FileLabels() {}
+
+// static
+void FileLabels::RegisterJSONConverter(
+ base::JSONValueConverter<FileLabels>* converter) {
+ converter->RegisterBoolField(kLabelStarred, &FileLabels::starred_);
+ converter->RegisterBoolField(kLabelHidden, &FileLabels::hidden_);
+ converter->RegisterBoolField(kLabelTrashed, &FileLabels::trashed_);
+ converter->RegisterBoolField(kLabelRestricted, &FileLabels::restricted_);
+ converter->RegisterBoolField(kLabelViewed, &FileLabels::viewed_);
+}
+
+// static
+scoped_ptr<FileLabels> FileLabels::CreateFrom(const base::Value& value) {
+ scoped_ptr<FileLabels> resource(new FileLabels());
+ if (!resource->Parse(value)) {
+ LOG(ERROR) << "Unable to create: Invalid FileLabels JSON!";
+ return scoped_ptr<FileLabels>();
+ }
+ return resource.Pass();
+}
+
+bool FileLabels::Parse(const base::Value& value) {
+ base::JSONValueConverter<FileLabels> converter;
+ if (!converter.Convert(value, this)) {
+ LOG(ERROR) << "Unable to parse: Invalid FileLabels.";
+ return false;
+ }
+ return true;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// ImageMediaMetadata implementation
+
+ImageMediaMetadata::ImageMediaMetadata()
+ : width_(-1),
+ height_(-1),
+ rotation_(-1) {}
+
+ImageMediaMetadata::~ImageMediaMetadata() {}
+
+// static
+void ImageMediaMetadata::RegisterJSONConverter(
+ base::JSONValueConverter<ImageMediaMetadata>* converter) {
+ converter->RegisterIntField(kImageMediaMetadataWidth,
+ &ImageMediaMetadata::width_);
+ converter->RegisterIntField(kImageMediaMetadataHeight,
+ &ImageMediaMetadata::height_);
+ converter->RegisterIntField(kImageMediaMetadataRotation,
+ &ImageMediaMetadata::rotation_);
+}
+
+// static
+scoped_ptr<ImageMediaMetadata> ImageMediaMetadata::CreateFrom(
+ const base::Value& value) {
+ scoped_ptr<ImageMediaMetadata> resource(new ImageMediaMetadata());
+ if (!resource->Parse(value)) {
+ LOG(ERROR) << "Unable to create: Invalid ImageMediaMetadata JSON!";
+ return scoped_ptr<ImageMediaMetadata>();
+ }
+ return resource.Pass();
+}
+
+bool ImageMediaMetadata::Parse(const base::Value& value) {
+ return true;
+ base::JSONValueConverter<ImageMediaMetadata> converter;
+ if (!converter.Convert(value, this)) {
+ LOG(ERROR) << "Unable to parse: Invalid ImageMediaMetadata.";
+ return false;
+ }
+ return true;
+}
+
+} // namespace google_apis
diff --git a/chromium/google_apis/drive/drive_api_parser.h b/chromium/google_apis/drive/drive_api_parser.h
new file mode 100644
index 00000000000..ef02035d595
--- /dev/null
+++ b/chromium/google_apis/drive/drive_api_parser.h
@@ -0,0 +1,857 @@
+// Copyright (c) 2012 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef GOOGLE_APIS_DRIVE_DRIVE_API_PARSER_H_
+#define GOOGLE_APIS_DRIVE_DRIVE_API_PARSER_H_
+
+#include <string>
+
+#include "base/compiler_specific.h"
+#include "base/gtest_prod_util.h"
+#include "base/memory/scoped_ptr.h"
+#include "base/memory/scoped_vector.h"
+#include "base/strings/string_piece.h"
+#include "base/time/time.h"
+#include "url/gurl.h"
+
+namespace base {
+class Value;
+template <class StructType>
+class JSONValueConverter;
+
+namespace internal {
+template <class NestedType>
+class RepeatedMessageConverter;
+} // namespace internal
+} // namespace base
+
+namespace google_apis {
+
+// About resource represents the account information about the current user.
+// https://developers.google.com/drive/v2/reference/about
+class AboutResource {
+ public:
+ AboutResource();
+ ~AboutResource();
+
+ // Registers the mapping between JSON field names and the members in this
+ // class.
+ static void RegisterJSONConverter(
+ base::JSONValueConverter<AboutResource>* converter);
+
+ // Creates about resource from parsed JSON.
+ static scoped_ptr<AboutResource> CreateFrom(const base::Value& value);
+
+ // Returns the largest change ID number.
+ int64 largest_change_id() const { return largest_change_id_; }
+ // Returns total number of quota bytes.
+ int64 quota_bytes_total() const { return quota_bytes_total_; }
+ // Returns the number of quota bytes used.
+ int64 quota_bytes_used() const { return quota_bytes_used_; }
+ // Returns root folder ID.
+ const std::string& root_folder_id() const { return root_folder_id_; }
+
+ void set_largest_change_id(int64 largest_change_id) {
+ largest_change_id_ = largest_change_id;
+ }
+ void set_quota_bytes_total(int64 quota_bytes_total) {
+ quota_bytes_total_ = quota_bytes_total;
+ }
+ void set_quota_bytes_used(int64 quota_bytes_used) {
+ quota_bytes_used_ = quota_bytes_used;
+ }
+ void set_root_folder_id(const std::string& root_folder_id) {
+ root_folder_id_ = root_folder_id;
+ }
+
+ private:
+ friend class DriveAPIParserTest;
+ FRIEND_TEST_ALL_PREFIXES(DriveAPIParserTest, AboutResourceParser);
+
+ // Parses and initializes data members from content of |value|.
+ // Return false if parsing fails.
+ bool Parse(const base::Value& value);
+
+ int64 largest_change_id_;
+ int64 quota_bytes_total_;
+ int64 quota_bytes_used_;
+ std::string root_folder_id_;
+
+ // This class is copyable on purpose.
+};
+
+// DriveAppIcon represents an icon for Drive Application.
+// https://developers.google.com/drive/v2/reference/apps
+class DriveAppIcon {
+ public:
+ enum IconCategory {
+ UNKNOWN, // Uninitialized state.
+ DOCUMENT, // Icon for a file associated with the app.
+ APPLICATION, // Icon for the application.
+ SHARED_DOCUMENT, // Icon for a shared file associated with the app.
+ };
+
+ DriveAppIcon();
+ ~DriveAppIcon();
+
+ // Registers the mapping between JSON field names and the members in this
+ // class.
+ static void RegisterJSONConverter(
+ base::JSONValueConverter<DriveAppIcon>* converter);
+
+ // Creates drive app icon instance from parsed JSON.
+ static scoped_ptr<DriveAppIcon> CreateFrom(const base::Value& value);
+
+ // Category of the icon.
+ IconCategory category() const { return category_; }
+
+ // Size in pixels of one side of the icon (icons are always square).
+ int icon_side_length() const { return icon_side_length_; }
+
+ // Returns URL for this icon.
+ const GURL& icon_url() const { return icon_url_; }
+
+ void set_category(IconCategory category) {
+ category_ = category;
+ }
+ void set_icon_side_length(int icon_side_length) {
+ icon_side_length_ = icon_side_length;
+ }
+ void set_icon_url(const GURL& icon_url) {
+ icon_url_ = icon_url;
+ }
+
+ private:
+ // Parses and initializes data members from content of |value|.
+ // Return false if parsing fails.
+ bool Parse(const base::Value& value);
+
+ // Extracts the icon category from the given string. Returns false and does
+ // not change |result| when |scheme| has an unrecognizable value.
+ static bool GetIconCategory(const base::StringPiece& category,
+ IconCategory* result);
+
+ friend class base::internal::RepeatedMessageConverter<DriveAppIcon>;
+ friend class AppResource;
+
+ IconCategory category_;
+ int icon_side_length_;
+ GURL icon_url_;
+
+ DISALLOW_COPY_AND_ASSIGN(DriveAppIcon);
+};
+
+// AppResource represents a Drive Application.
+// https://developers.google.com/drive/v2/reference/apps
+class AppResource {
+ public:
+ ~AppResource();
+ AppResource();
+
+ // Registers the mapping between JSON field names and the members in this
+ // class.
+ static void RegisterJSONConverter(
+ base::JSONValueConverter<AppResource>* converter);
+
+ // Creates app resource from parsed JSON.
+ static scoped_ptr<AppResource> CreateFrom(const base::Value& value);
+
+ // Returns application ID, which is 12-digit decimals (e.g. "123456780123").
+ const std::string& application_id() const { return application_id_; }
+
+ // Returns application name.
+ const std::string& name() const { return name_; }
+
+ // Returns the name of the type of object this application creates.
+ // This is used for displaying in "Create" menu item for this app.
+ // If empty, application name is used instead.
+ const std::string& object_type() const { return object_type_; }
+
+ // Returns whether this application supports creating new objects.
+ bool supports_create() const { return supports_create_; }
+
+ // Returns whether this application supports importing Google Docs.
+ bool supports_import() const { return supports_import_; }
+
+ // Returns whether this application is installed.
+ bool is_installed() const { return installed_; }
+
+ // Returns whether this application is authorized to access data on the
+ // user's Drive.
+ bool is_authorized() const { return authorized_; }
+
+ // Returns the product URL, e.g. at Chrome Web Store.
+ const GURL& product_url() const { return product_url_; }
+
+ // List of primary mime types supported by this WebApp. Primary status should
+ // trigger this WebApp becoming the default handler of file instances that
+ // have these mime types.
+ const ScopedVector<std::string>& primary_mimetypes() const {
+ return primary_mimetypes_;
+ }
+
+ // List of secondary mime types supported by this WebApp. Secondary status
+ // should make this WebApp show up in "Open with..." pop-up menu of the
+ // default action menu for file with matching mime types.
+ const ScopedVector<std::string>& secondary_mimetypes() const {
+ return secondary_mimetypes_;
+ }
+
+ // List of primary file extensions supported by this WebApp. Primary status
+ // should trigger this WebApp becoming the default handler of file instances
+ // that match these extensions.
+ const ScopedVector<std::string>& primary_file_extensions() const {
+ return primary_file_extensions_;
+ }
+
+ // List of secondary file extensions supported by this WebApp. Secondary
+ // status should make this WebApp show up in "Open with..." pop-up menu of the
+ // default action menu for file with matching extensions.
+ const ScopedVector<std::string>& secondary_file_extensions() const {
+ return secondary_file_extensions_;
+ }
+
+ // Returns Icons for this application. An application can have multiple
+ // icons for different purpose (application, document, shared document)
+ // in several sizes.
+ const ScopedVector<DriveAppIcon>& icons() const {
+ return icons_;
+ }
+
+ void set_application_id(const std::string& application_id) {
+ application_id_ = application_id;
+ }
+ void set_name(const std::string& name) { name_ = name; }
+ void set_object_type(const std::string& object_type) {
+ object_type_ = object_type;
+ }
+ void set_supports_create(bool supports_create) {
+ supports_create_ = supports_create;
+ }
+ void set_supports_import(bool supports_import) {
+ supports_import_ = supports_import;
+ }
+ void set_installed(bool installed) { installed_ = installed; }
+ void set_authorized(bool authorized) { authorized_ = authorized; }
+ void set_product_url(const GURL& product_url) {
+ product_url_ = product_url;
+ }
+ void set_primary_mimetypes(
+ ScopedVector<std::string> primary_mimetypes) {
+ primary_mimetypes_ = primary_mimetypes.Pass();
+ }
+ void set_secondary_mimetypes(
+ ScopedVector<std::string> secondary_mimetypes) {
+ secondary_mimetypes_ = secondary_mimetypes.Pass();
+ }
+ void set_primary_file_extensions(
+ ScopedVector<std::string> primary_file_extensions) {
+ primary_file_extensions_ = primary_file_extensions.Pass();
+ }
+ void set_secondary_file_extensions(
+ ScopedVector<std::string> secondary_file_extensions) {
+ secondary_file_extensions_ = secondary_file_extensions.Pass();
+ }
+ void set_icons(ScopedVector<DriveAppIcon> icons) {
+ icons_ = icons.Pass();
+ }
+
+ private:
+ friend class base::internal::RepeatedMessageConverter<AppResource>;
+ friend class AppList;
+
+ // Parses and initializes data members from content of |value|.
+ // Return false if parsing fails.
+ bool Parse(const base::Value& value);
+
+ std::string application_id_;
+ std::string name_;
+ std::string object_type_;
+ bool supports_create_;
+ bool supports_import_;
+ bool installed_;
+ bool authorized_;
+ GURL product_url_;
+ ScopedVector<std::string> primary_mimetypes_;
+ ScopedVector<std::string> secondary_mimetypes_;
+ ScopedVector<std::string> primary_file_extensions_;
+ ScopedVector<std::string> secondary_file_extensions_;
+ ScopedVector<DriveAppIcon> icons_;
+
+ DISALLOW_COPY_AND_ASSIGN(AppResource);
+};
+
+// AppList represents a list of Drive Applications.
+// https://developers.google.com/drive/v2/reference/apps/list
+class AppList {
+ public:
+ AppList();
+ ~AppList();
+
+ // Registers the mapping between JSON field names and the members in this
+ // class.
+ static void RegisterJSONConverter(
+ base::JSONValueConverter<AppList>* converter);
+
+ // Creates app list from parsed JSON.
+ static scoped_ptr<AppList> CreateFrom(const base::Value& value);
+
+ // ETag for this resource.
+ const std::string& etag() const { return etag_; }
+
+ // Returns a vector of applications.
+ const ScopedVector<AppResource>& items() const { return items_; }
+
+ void set_etag(const std::string& etag) {
+ etag_ = etag;
+ }
+ void set_items(ScopedVector<AppResource> items) {
+ items_ = items.Pass();
+ }
+
+ private:
+ friend class DriveAPIParserTest;
+ FRIEND_TEST_ALL_PREFIXES(DriveAPIParserTest, AppListParser);
+
+ // Parses and initializes data members from content of |value|.
+ // Return false if parsing fails.
+ bool Parse(const base::Value& value);
+
+ std::string etag_;
+ ScopedVector<AppResource> items_;
+
+ DISALLOW_COPY_AND_ASSIGN(AppList);
+};
+
+// ParentReference represents a directory.
+// https://developers.google.com/drive/v2/reference/parents
+class ParentReference {
+ public:
+ ParentReference();
+ ~ParentReference();
+
+ // Registers the mapping between JSON field names and the members in this
+ // class.
+ static void RegisterJSONConverter(
+ base::JSONValueConverter<ParentReference>* converter);
+
+ // Creates parent reference from parsed JSON.
+ static scoped_ptr<ParentReference> CreateFrom(const base::Value& value);
+
+ // Returns the file id of the reference.
+ const std::string& file_id() const { return file_id_; }
+
+ // Returns the URL for the parent in Drive.
+ const GURL& parent_link() const { return parent_link_; }
+
+ // Returns true if the reference is root directory.
+ bool is_root() const { return is_root_; }
+
+ void set_file_id(const std::string& file_id) { file_id_ = file_id; }
+ void set_parent_link(const GURL& parent_link) {
+ parent_link_ = parent_link;
+ }
+ void set_is_root(bool is_root) { is_root_ = is_root; }
+
+ private:
+ friend class base::internal::RepeatedMessageConverter<ParentReference>;
+
+ // Parses and initializes data members from content of |value|.
+ // Return false if parsing fails.
+ bool Parse(const base::Value& value);
+
+ std::string file_id_;
+ GURL parent_link_;
+ bool is_root_;
+
+ DISALLOW_COPY_AND_ASSIGN(ParentReference);
+};
+
+// FileLabels represents labels for file or folder.
+// https://developers.google.com/drive/v2/reference/files
+class FileLabels {
+ public:
+ FileLabels();
+ ~FileLabels();
+
+ // Registers the mapping between JSON field names and the members in this
+ // class.
+ static void RegisterJSONConverter(
+ base::JSONValueConverter<FileLabels>* converter);
+
+ // Creates about resource from parsed JSON.
+ static scoped_ptr<FileLabels> CreateFrom(const base::Value& value);
+
+ // Whether this file is starred by the user.
+ bool is_starred() const { return starred_; }
+ // Whether this file is hidden from the user.
+ bool is_hidden() const { return hidden_; }
+ // Whether this file has been trashed.
+ bool is_trashed() const { return trashed_; }
+ // Whether viewers are prevented from downloading this file.
+ bool is_restricted() const { return restricted_; }
+ // Whether this file has been viewed by this user.
+ bool is_viewed() const { return viewed_; }
+
+ void set_starred(bool starred) { starred_ = starred; }
+ void set_hidden(bool hidden) { hidden_ = hidden; }
+ void set_trashed(bool trashed) { trashed_ = trashed; }
+ void set_restricted(bool restricted) { restricted_ = restricted; }
+ void set_viewed(bool viewed) { viewed_ = viewed; }
+
+ private:
+ friend class FileResource;
+
+ // Parses and initializes data members from content of |value|.
+ // Return false if parsing fails.
+ bool Parse(const base::Value& value);
+
+ bool starred_;
+ bool hidden_;
+ bool trashed_;
+ bool restricted_;
+ bool viewed_;
+
+ DISALLOW_COPY_AND_ASSIGN(FileLabels);
+};
+
+// ImageMediaMetadata represents image metadata for a file.
+// https://developers.google.com/drive/v2/reference/files
+class ImageMediaMetadata {
+ public:
+ ImageMediaMetadata();
+ ~ImageMediaMetadata();
+
+ // Registers the mapping between JSON field names and the members in this
+ // class.
+ static void RegisterJSONConverter(
+ base::JSONValueConverter<ImageMediaMetadata>* converter);
+
+ // Creates about resource from parsed JSON.
+ static scoped_ptr<ImageMediaMetadata> CreateFrom(const base::Value& value);
+
+ // Width of the image in pixels.
+ int width() const { return width_; }
+ // Height of the image in pixels.
+ int height() const { return height_; }
+ // Rotation of the image in clockwise degrees.
+ int rotation() const { return rotation_; }
+
+ void set_width(int width) { width_ = width; }
+ void set_height(int height) { height_ = height; }
+ void set_rotation(int rotation) { rotation_ = rotation; }
+
+ private:
+ friend class FileResource;
+
+ // Parses and initializes data members from content of |value|.
+ // Return false if parsing fails.
+ bool Parse(const base::Value& value);
+
+ int width_;
+ int height_;
+ int rotation_;
+
+ DISALLOW_COPY_AND_ASSIGN(ImageMediaMetadata);
+};
+
+
+// FileResource represents a file or folder metadata in Drive.
+// https://developers.google.com/drive/v2/reference/files
+class FileResource {
+ public:
+ // Link to open a file resource on a web app with |app_id|.
+ struct OpenWithLink {
+ std::string app_id;
+ GURL open_url;
+ };
+
+ FileResource();
+ ~FileResource();
+
+ // Registers the mapping between JSON field names and the members in this
+ // class.
+ static void RegisterJSONConverter(
+ base::JSONValueConverter<FileResource>* converter);
+
+ // Creates file resource from parsed JSON.
+ static scoped_ptr<FileResource> CreateFrom(const base::Value& value);
+
+ // Returns true if this is a directory.
+ // Note: "folder" is used elsewhere in this file to match Drive API reference,
+ // but outside this file we use "directory" to match HTML5 filesystem API.
+ bool IsDirectory() const;
+
+ // Returns file ID. This is unique in all files in Google Drive.
+ const std::string& file_id() const { return file_id_; }
+
+ // Returns ETag for this file.
+ const std::string& etag() const { return etag_; }
+
+ // Returns the link to JSON of this file itself.
+ const GURL& self_link() const { return self_link_; }
+
+ // Returns the title of this file.
+ const std::string& title() const { return title_; }
+
+ // Returns MIME type of this file.
+ const std::string& mime_type() const { return mime_type_; }
+
+ // Returns labels for this file.
+ const FileLabels& labels() const { return labels_; }
+
+ // Returns image media metadata for this file.
+ const ImageMediaMetadata& image_media_metadata() const {
+ return image_media_metadata_;
+ }
+
+ // Returns created time of this file.
+ const base::Time& created_date() const { return created_date_; }
+
+ // Returns modified time of this file.
+ const base::Time& modified_date() const { return modified_date_; }
+
+ // Returns modification time by the user.
+ const base::Time& modified_by_me_date() const { return modified_by_me_date_; }
+
+ // Returns last access time by the user.
+ const base::Time& last_viewed_by_me_date() const {
+ return last_viewed_by_me_date_;
+ }
+
+ // Returns time when the file was shared with the user.
+ const base::Time& shared_with_me_date() const {
+ return shared_with_me_date_;
+ }
+
+ // Returns the 'shared' attribute of the file.
+ bool shared() const { return shared_; }
+
+ // Returns the short-lived download URL for the file. This field exists
+ // only when the file content is stored in Drive.
+ const GURL& download_url() const { return download_url_; }
+
+ // Returns the extension part of the filename.
+ const std::string& file_extension() const { return file_extension_; }
+
+ // Returns MD5 checksum of this file.
+ const std::string& md5_checksum() const { return md5_checksum_; }
+
+ // Returns the size of this file in bytes.
+ int64 file_size() const { return file_size_; }
+
+ // Return the link to open the file in Google editor or viewer.
+ // E.g. Google Document, Google Spreadsheet.
+ const GURL& alternate_link() const { return alternate_link_; }
+
+ // Returns the link for embedding the file.
+ const GURL& embed_link() const { return embed_link_; }
+
+ // Returns parent references (directories) of this file.
+ const ScopedVector<ParentReference>& parents() const { return parents_; }
+
+ // Returns the link to the file's thumbnail.
+ const GURL& thumbnail_link() const { return thumbnail_link_; }
+
+ // Returns the link to open its downloadable content, using cookie based
+ // authentication.
+ const GURL& web_content_link() const { return web_content_link_; }
+
+ // Returns the list of links to open the resource with a web app.
+ const std::vector<OpenWithLink>& open_with_links() const {
+ return open_with_links_;
+ }
+
+ void set_file_id(const std::string& file_id) {
+ file_id_ = file_id;
+ }
+ void set_etag(const std::string& etag) {
+ etag_ = etag;
+ }
+ void set_self_link(const GURL& self_link) {
+ self_link_ = self_link;
+ }
+ void set_title(const std::string& title) {
+ title_ = title;
+ }
+ void set_mime_type(const std::string& mime_type) {
+ mime_type_ = mime_type;
+ }
+ FileLabels* mutable_labels() {
+ return &labels_;
+ }
+ ImageMediaMetadata* mutable_image_media_metadata() {
+ return &image_media_metadata_;
+ }
+ void set_created_date(const base::Time& created_date) {
+ created_date_ = created_date;
+ }
+ void set_modified_date(const base::Time& modified_date) {
+ modified_date_ = modified_date;
+ }
+ void set_modified_by_me_date(const base::Time& modified_by_me_date) {
+ modified_by_me_date_ = modified_by_me_date;
+ }
+ void set_last_viewed_by_me_date(const base::Time& last_viewed_by_me_date) {
+ last_viewed_by_me_date_ = last_viewed_by_me_date;
+ }
+ void set_shared_with_me_date(const base::Time& shared_with_me_date) {
+ shared_with_me_date_ = shared_with_me_date;
+ }
+ void set_shared(bool shared) {
+ shared_ = shared;
+ }
+ void set_download_url(const GURL& download_url) {
+ download_url_ = download_url;
+ }
+ void set_file_extension(const std::string& file_extension) {
+ file_extension_ = file_extension;
+ }
+ void set_md5_checksum(const std::string& md5_checksum) {
+ md5_checksum_ = md5_checksum;
+ }
+ void set_file_size(int64 file_size) {
+ file_size_ = file_size;
+ }
+ void set_alternate_link(const GURL& alternate_link) {
+ alternate_link_ = alternate_link;
+ }
+ void set_embed_link(const GURL& embed_link) {
+ embed_link_ = embed_link;
+ }
+ void set_parents(ScopedVector<ParentReference> parents) {
+ parents_ = parents.Pass();
+ }
+ void set_thumbnail_link(const GURL& thumbnail_link) {
+ thumbnail_link_ = thumbnail_link;
+ }
+ void set_web_content_link(const GURL& web_content_link) {
+ web_content_link_ = web_content_link;
+ }
+
+ private:
+ friend class base::internal::RepeatedMessageConverter<FileResource>;
+ friend class ChangeResource;
+ friend class FileList;
+
+ // Parses and initializes data members from content of |value|.
+ // Return false if parsing fails.
+ bool Parse(const base::Value& value);
+
+ std::string file_id_;
+ std::string etag_;
+ GURL self_link_;
+ std::string title_;
+ std::string mime_type_;
+ FileLabels labels_;
+ ImageMediaMetadata image_media_metadata_;
+ base::Time created_date_;
+ base::Time modified_date_;
+ base::Time modified_by_me_date_;
+ base::Time last_viewed_by_me_date_;
+ base::Time shared_with_me_date_;
+ bool shared_;
+ GURL download_url_;
+ std::string file_extension_;
+ std::string md5_checksum_;
+ int64 file_size_;
+ GURL alternate_link_;
+ GURL embed_link_;
+ ScopedVector<ParentReference> parents_;
+ GURL thumbnail_link_;
+ GURL web_content_link_;
+ std::vector<OpenWithLink> open_with_links_;
+
+ DISALLOW_COPY_AND_ASSIGN(FileResource);
+};
+
+// FileList represents a collection of files and folders.
+// https://developers.google.com/drive/v2/reference/files/list
+class FileList {
+ public:
+ FileList();
+ ~FileList();
+
+ // Registers the mapping between JSON field names and the members in this
+ // class.
+ static void RegisterJSONConverter(
+ base::JSONValueConverter<FileList>* converter);
+
+ // Returns true if the |value| has kind field for FileList.
+ static bool HasFileListKind(const base::Value& value);
+
+ // Creates file list from parsed JSON.
+ static scoped_ptr<FileList> CreateFrom(const base::Value& value);
+
+ // Returns the ETag of the list.
+ const std::string& etag() const { return etag_; }
+
+ // Returns the page token for the next page of files, if the list is large
+ // to fit in one response. If this is empty, there is no more file lists.
+ const std::string& next_page_token() const { return next_page_token_; }
+
+ // Returns a link to the next page of files. The URL includes the next page
+ // token.
+ const GURL& next_link() const { return next_link_; }
+
+ // Returns a set of files in this list.
+ const ScopedVector<FileResource>& items() const { return items_; }
+
+ void set_etag(const std::string& etag) {
+ etag_ = etag;
+ }
+ void set_next_page_token(const std::string& next_page_token) {
+ next_page_token_ = next_page_token;
+ }
+ void set_next_link(const GURL& next_link) {
+ next_link_ = next_link;
+ }
+ void set_items(ScopedVector<FileResource> items) {
+ items_ = items.Pass();
+ }
+
+ private:
+ friend class DriveAPIParserTest;
+ FRIEND_TEST_ALL_PREFIXES(DriveAPIParserTest, FileListParser);
+
+ // Parses and initializes data members from content of |value|.
+ // Return false if parsing fails.
+ bool Parse(const base::Value& value);
+
+ std::string etag_;
+ std::string next_page_token_;
+ GURL next_link_;
+ ScopedVector<FileResource> items_;
+
+ DISALLOW_COPY_AND_ASSIGN(FileList);
+};
+
+// ChangeResource represents a change in a file.
+// https://developers.google.com/drive/v2/reference/changes
+class ChangeResource {
+ public:
+ ChangeResource();
+ ~ChangeResource();
+
+ // Registers the mapping between JSON field names and the members in this
+ // class.
+ static void RegisterJSONConverter(
+ base::JSONValueConverter<ChangeResource>* converter);
+
+ // Creates change resource from parsed JSON.
+ static scoped_ptr<ChangeResource> CreateFrom(const base::Value& value);
+
+ // Returns change ID for this change. This is a monotonically increasing
+ // number.
+ int64 change_id() const { return change_id_; }
+
+ // Returns a string file ID for corresponding file of the change.
+ const std::string& file_id() const { return file_id_; }
+
+ // Returns true if this file is deleted in the change.
+ bool is_deleted() const { return deleted_; }
+
+ // Returns FileResource of the file which the change refers to.
+ const FileResource* file() const { return file_.get(); }
+
+ void set_change_id(int64 change_id) {
+ change_id_ = change_id;
+ }
+ void set_file_id(const std::string& file_id) {
+ file_id_ = file_id;
+ }
+ void set_deleted(bool deleted) {
+ deleted_ = deleted;
+ }
+ void set_file(scoped_ptr<FileResource> file) {
+ file_ = file.Pass();
+ }
+
+ private:
+ friend class base::internal::RepeatedMessageConverter<ChangeResource>;
+ friend class ChangeList;
+
+ // Parses and initializes data members from content of |value|.
+ // Return false if parsing fails.
+ bool Parse(const base::Value& value);
+
+ int64 change_id_;
+ std::string file_id_;
+ bool deleted_;
+ scoped_ptr<FileResource> file_;
+
+ DISALLOW_COPY_AND_ASSIGN(ChangeResource);
+};
+
+// ChangeList represents a set of changes in the drive.
+// https://developers.google.com/drive/v2/reference/changes/list
+class ChangeList {
+ public:
+ ChangeList();
+ ~ChangeList();
+
+ // Registers the mapping between JSON field names and the members in this
+ // class.
+ static void RegisterJSONConverter(
+ base::JSONValueConverter<ChangeList>* converter);
+
+ // Returns true if the |value| has kind field for ChangeList.
+ static bool HasChangeListKind(const base::Value& value);
+
+ // Creates change list from parsed JSON.
+ static scoped_ptr<ChangeList> CreateFrom(const base::Value& value);
+
+ // Returns the ETag of the list.
+ const std::string& etag() const { return etag_; }
+
+ // Returns the page token for the next page of files, if the list is large
+ // to fit in one response. If this is empty, there is no more file lists.
+ const std::string& next_page_token() const { return next_page_token_; }
+
+ // Returns a link to the next page of files. The URL includes the next page
+ // token.
+ const GURL& next_link() const { return next_link_; }
+
+ // Returns the largest change ID number.
+ int64 largest_change_id() const { return largest_change_id_; }
+
+ // Returns a set of changes in this list.
+ const ScopedVector<ChangeResource>& items() const { return items_; }
+
+ void set_etag(const std::string& etag) {
+ etag_ = etag;
+ }
+ void set_next_page_token(const std::string& next_page_token) {
+ next_page_token_ = next_page_token;
+ }
+ void set_next_link(const GURL& next_link) {
+ next_link_ = next_link;
+ }
+ void set_largest_change_id(int64 largest_change_id) {
+ largest_change_id_ = largest_change_id;
+ }
+ void set_items(ScopedVector<ChangeResource> items) {
+ items_ = items.Pass();
+ }
+
+ private:
+ friend class DriveAPIParserTest;
+ FRIEND_TEST_ALL_PREFIXES(DriveAPIParserTest, ChangeListParser);
+
+ // Parses and initializes data members from content of |value|.
+ // Return false if parsing fails.
+ bool Parse(const base::Value& value);
+
+ std::string etag_;
+ std::string next_page_token_;
+ GURL next_link_;
+ int64 largest_change_id_;
+ ScopedVector<ChangeResource> items_;
+
+ DISALLOW_COPY_AND_ASSIGN(ChangeList);
+};
+
+} // namespace google_apis
+
+#endif // GOOGLE_APIS_DRIVE_DRIVE_API_PARSER_H_
diff --git a/chromium/google_apis/drive/drive_api_parser_unittest.cc b/chromium/google_apis/drive/drive_api_parser_unittest.cc
new file mode 100644
index 00000000000..0960b275bf5
--- /dev/null
+++ b/chromium/google_apis/drive/drive_api_parser_unittest.cc
@@ -0,0 +1,304 @@
+// Copyright (c) 2012 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "google_apis/drive/drive_api_parser.h"
+
+#include "base/time/time.h"
+#include "base/values.h"
+#include "google_apis/drive/gdata_wapi_parser.h"
+#include "google_apis/drive/test_util.h"
+#include "google_apis/drive/time_util.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+namespace google_apis {
+
+// Test about resource parsing.
+TEST(DriveAPIParserTest, AboutResourceParser) {
+ std::string error;
+ scoped_ptr<base::Value> document = test_util::LoadJSONFile(
+ "drive/about.json");
+ ASSERT_TRUE(document.get());
+
+ ASSERT_EQ(base::Value::TYPE_DICTIONARY, document->GetType());
+ scoped_ptr<AboutResource> resource(new AboutResource());
+ EXPECT_TRUE(resource->Parse(*document));
+
+ EXPECT_EQ("0AIv7G8yEYAWHUk9123", resource->root_folder_id());
+ EXPECT_EQ(5368709120LL, resource->quota_bytes_total());
+ EXPECT_EQ(1073741824LL, resource->quota_bytes_used());
+ EXPECT_EQ(8177LL, resource->largest_change_id());
+}
+
+// Test app list parsing.
+TEST(DriveAPIParserTest, AppListParser) {
+ std::string error;
+ scoped_ptr<base::Value> document = test_util::LoadJSONFile(
+ "drive/applist.json");
+ ASSERT_TRUE(document.get());
+
+ ASSERT_EQ(base::Value::TYPE_DICTIONARY, document->GetType());
+ scoped_ptr<AppList> applist(new AppList);
+ EXPECT_TRUE(applist->Parse(*document));
+
+ EXPECT_EQ("\"Jm4BaSnCWNND-noZsHINRqj4ABC/tuqRBw0lvjUdPtc_2msA1tN4XYZ\"",
+ applist->etag());
+ ASSERT_EQ(2U, applist->items().size());
+ // Check Drive app 1
+ const AppResource& app1 = *applist->items()[0];
+ EXPECT_EQ("123456788192", app1.application_id());
+ EXPECT_EQ("Drive app 1", app1.name());
+ EXPECT_EQ("", app1.object_type());
+ EXPECT_TRUE(app1.supports_create());
+ EXPECT_TRUE(app1.supports_import());
+ EXPECT_TRUE(app1.is_installed());
+ EXPECT_FALSE(app1.is_authorized());
+ EXPECT_EQ("https://chrome.google.com/webstore/detail/"
+ "abcdefghabcdefghabcdefghabcdefgh",
+ app1.product_url().spec());
+
+ ASSERT_EQ(1U, app1.primary_mimetypes().size());
+ EXPECT_EQ("application/vnd.google-apps.drive-sdk.123456788192",
+ *app1.primary_mimetypes()[0]);
+
+ ASSERT_EQ(2U, app1.secondary_mimetypes().size());
+ EXPECT_EQ("text/html", *app1.secondary_mimetypes()[0]);
+ EXPECT_EQ("text/plain", *app1.secondary_mimetypes()[1]);
+
+ ASSERT_EQ(2U, app1.primary_file_extensions().size());
+ EXPECT_EQ("exe", *app1.primary_file_extensions()[0]);
+ EXPECT_EQ("com", *app1.primary_file_extensions()[1]);
+
+ EXPECT_EQ(0U, app1.secondary_file_extensions().size());
+
+ ASSERT_EQ(6U, app1.icons().size());
+ const DriveAppIcon& icon1 = *app1.icons()[0];
+ EXPECT_EQ(DriveAppIcon::APPLICATION, icon1.category());
+ EXPECT_EQ(10, icon1.icon_side_length());
+ EXPECT_EQ("http://www.example.com/10.png", icon1.icon_url().spec());
+
+ const DriveAppIcon& icon6 = *app1.icons()[5];
+ EXPECT_EQ(DriveAppIcon::SHARED_DOCUMENT, icon6.category());
+ EXPECT_EQ(16, icon6.icon_side_length());
+ EXPECT_EQ("http://www.example.com/ds16.png", icon6.icon_url().spec());
+
+ // Check Drive app 2
+ const AppResource& app2 = *applist->items()[1];
+ EXPECT_EQ("876543210000", app2.application_id());
+ EXPECT_EQ("Drive app 2", app2.name());
+ EXPECT_EQ("", app2.object_type());
+ EXPECT_FALSE(app2.supports_create());
+ EXPECT_FALSE(app2.supports_import());
+ EXPECT_TRUE(app2.is_installed());
+ EXPECT_FALSE(app2.is_authorized());
+ EXPECT_EQ("https://chrome.google.com/webstore/detail/"
+ "hgfedcbahgfedcbahgfedcbahgfedcba",
+ app2.product_url().spec());
+
+ ASSERT_EQ(3U, app2.primary_mimetypes().size());
+ EXPECT_EQ("image/jpeg", *app2.primary_mimetypes()[0]);
+ EXPECT_EQ("image/png", *app2.primary_mimetypes()[1]);
+ EXPECT_EQ("application/vnd.google-apps.drive-sdk.876543210000",
+ *app2.primary_mimetypes()[2]);
+
+ EXPECT_EQ(0U, app2.secondary_mimetypes().size());
+ EXPECT_EQ(0U, app2.primary_file_extensions().size());
+ EXPECT_EQ(0U, app2.secondary_file_extensions().size());
+
+ ASSERT_EQ(3U, app2.icons().size());
+ const DriveAppIcon& icon2 = *app2.icons()[1];
+ EXPECT_EQ(DriveAppIcon::DOCUMENT, icon2.category());
+ EXPECT_EQ(10, icon2.icon_side_length());
+ EXPECT_EQ("http://www.example.com/d10.png", icon2.icon_url().spec());
+}
+
+// Test file list parsing.
+TEST(DriveAPIParserTest, FileListParser) {
+ std::string error;
+ scoped_ptr<base::Value> document = test_util::LoadJSONFile(
+ "drive/filelist.json");
+ ASSERT_TRUE(document.get());
+
+ ASSERT_EQ(base::Value::TYPE_DICTIONARY, document->GetType());
+ scoped_ptr<FileList> filelist(new FileList);
+ EXPECT_TRUE(filelist->Parse(*document));
+
+ EXPECT_EQ("\"WtRjAPZWbDA7_fkFjc5ojsEvDEF/zyHTfoHpnRHovyi8bWpwK0DXABC\"",
+ filelist->etag());
+ EXPECT_EQ("EAIaggELEgA6egpi96It9mH_____f_8AAP__AAD_okhU-cHLz83KzszMxsjMzs_Ry"
+ "NGJnridyrbHs7u9tv8AAP__AP7__n__AP8AokhU-cHLz83KzszMxsjMzs_RyNGJnr"
+ "idyrbHs7u9tv8A__4QZCEiXPTi_wtIgTkAAAAAngnSXUgCDEAAIgsJPgart10AAAA"
+ "ABC", filelist->next_page_token());
+ EXPECT_EQ(GURL("https://www.googleapis.com/drive/v2/files?pageToken=EAIaggEL"
+ "EgA6egpi96It9mH_____f_8AAP__AAD_okhU-cHLz83KzszMxsjMzs_RyNGJ"
+ "nridyrbHs7u9tv8AAP__AP7__n__AP8AokhU-cHLz83KzszMxsjMzs_RyNGJ"
+ "nridyrbHs7u9tv8A__4QZCEiXPTi_wtIgTkAAAAAngnSXUgCDEAAIgsJPgar"
+ "t10AAAAABC"), filelist->next_link());
+
+ ASSERT_EQ(3U, filelist->items().size());
+ // Check file 1 (a regular file)
+ const FileResource& file1 = *filelist->items()[0];
+ EXPECT_EQ("0B4v7G8yEYAWHUmRrU2lMS2hLABC", file1.file_id());
+ EXPECT_EQ("\"WtRjAPZWbDA7_fkFjc5ojsEvDEF/MTM0MzM2NzgwMDIXYZ\"",
+ file1.etag());
+ EXPECT_EQ("My first file data", file1.title());
+ EXPECT_EQ("application/octet-stream", file1.mime_type());
+
+ EXPECT_FALSE(file1.labels().is_starred());
+ EXPECT_FALSE(file1.labels().is_hidden());
+ EXPECT_FALSE(file1.labels().is_trashed());
+ EXPECT_FALSE(file1.labels().is_restricted());
+ EXPECT_TRUE(file1.labels().is_viewed());
+ EXPECT_FALSE(file1.shared());
+
+ EXPECT_EQ(640, file1.image_media_metadata().width());
+ EXPECT_EQ(480, file1.image_media_metadata().height());
+ EXPECT_EQ(90, file1.image_media_metadata().rotation());
+
+ base::Time created_time;
+ ASSERT_TRUE(
+ util::GetTimeFromString("2012-07-24T08:51:16.570Z", &created_time));
+ EXPECT_EQ(created_time, file1.created_date());
+
+ base::Time modified_time;
+ ASSERT_TRUE(
+ util::GetTimeFromString("2012-07-27T05:43:20.269Z", &modified_time));
+ EXPECT_EQ(modified_time, file1.modified_date());
+ EXPECT_EQ(modified_time, file1.modified_by_me_date());
+
+ ASSERT_EQ(1U, file1.parents().size());
+ EXPECT_EQ("0B4v7G8yEYAWHYW1OcExsUVZLABC", file1.parents()[0]->file_id());
+ EXPECT_EQ(GURL("https://www.googleapis.com/drive/v2/files/"
+ "0B4v7G8yEYAWHYW1OcExsUVZLABC"),
+ file1.parents()[0]->parent_link());
+ EXPECT_FALSE(file1.parents()[0]->is_root());
+
+ EXPECT_EQ(GURL("https://www.example.com/download"), file1.download_url());
+ EXPECT_EQ("ext", file1.file_extension());
+ EXPECT_EQ("d41d8cd98f00b204e9800998ecf8427e", file1.md5_checksum());
+ EXPECT_EQ(1000U, file1.file_size());
+
+ EXPECT_EQ(GURL("https://www.googleapis.com/drive/v2/files/"
+ "0B4v7G8yEYAWHUmRrU2lMS2hLABC"),
+ file1.self_link());
+ EXPECT_EQ(GURL("https://docs.google.com/file/d/"
+ "0B4v7G8yEYAWHUmRrU2lMS2hLABC/edit"),
+ file1.alternate_link());
+ EXPECT_EQ(GURL("https://docs.google.com/uc?"
+ "id=0B4v7G8yEYAWHUmRrU2lMS2hLABC&export=download"),
+ file1.web_content_link());
+ ASSERT_EQ(1U, file1.open_with_links().size());
+ EXPECT_EQ("1234567890", file1.open_with_links()[0].app_id);
+ EXPECT_EQ(GURL("http://open_with_link/url"),
+ file1.open_with_links()[0].open_url);
+
+ // Check file 2 (a Google Document)
+ const FileResource& file2 = *filelist->items()[1];
+ EXPECT_EQ("Test Google Document", file2.title());
+ EXPECT_EQ("application/vnd.google-apps.document", file2.mime_type());
+
+ EXPECT_TRUE(file2.labels().is_starred());
+ EXPECT_TRUE(file2.labels().is_hidden());
+ EXPECT_TRUE(file2.labels().is_trashed());
+ EXPECT_TRUE(file2.labels().is_restricted());
+ EXPECT_TRUE(file2.labels().is_viewed());
+ EXPECT_TRUE(file2.shared());
+
+ EXPECT_EQ(-1, file2.image_media_metadata().width());
+ EXPECT_EQ(-1, file2.image_media_metadata().height());
+ EXPECT_EQ(-1, file2.image_media_metadata().rotation());
+
+ base::Time shared_with_me_time;
+ ASSERT_TRUE(util::GetTimeFromString("2012-07-27T04:54:11.030Z",
+ &shared_with_me_time));
+ EXPECT_EQ(shared_with_me_time, file2.shared_with_me_date());
+
+ EXPECT_EQ(0U, file2.file_size());
+
+ ASSERT_EQ(0U, file2.parents().size());
+
+ EXPECT_EQ(GURL("https://docs.google.com/a/chromium.org/document/d/"
+ "1Pc8jzfU1ErbN_eucMMqdqzY3eBm0v8sxXm_1CtLxABC/preview"),
+ file2.embed_link());
+ EXPECT_EQ(GURL("https://docs.google.com/feeds/vt?gd=true&"
+ "id=1Pc8jzfU1ErbN_eucMMqdqzY3eBm0v8sxXm_1CtLxABC&"
+ "v=3&s=AMedNnoAAAAAUBJyB0g8HbxZaLRnlztxefZPS24LiXYZ&sz=s220"),
+ file2.thumbnail_link());
+ EXPECT_EQ(0U, file2.open_with_links().size());
+
+ // Check file 3 (a folder)
+ const FileResource& file3 = *filelist->items()[2];
+ EXPECT_EQ(0U, file3.file_size());
+ EXPECT_EQ("TestFolder", file3.title());
+ EXPECT_EQ("application/vnd.google-apps.folder", file3.mime_type());
+ ASSERT_TRUE(file3.IsDirectory());
+ EXPECT_FALSE(file3.shared());
+
+ ASSERT_EQ(1U, file3.parents().size());
+ EXPECT_EQ("0AIv7G8yEYAWHUk9ABC", file3.parents()[0]->file_id());
+ EXPECT_TRUE(file3.parents()[0]->is_root());
+ EXPECT_EQ(0U, file3.open_with_links().size());
+}
+
+// Test change list parsing.
+TEST(DriveAPIParserTest, ChangeListParser) {
+ std::string error;
+ scoped_ptr<base::Value> document =
+ test_util::LoadJSONFile("drive/changelist.json");
+ ASSERT_TRUE(document.get());
+
+ ASSERT_EQ(base::Value::TYPE_DICTIONARY, document->GetType());
+ scoped_ptr<ChangeList> changelist(new ChangeList);
+ EXPECT_TRUE(changelist->Parse(*document));
+
+ EXPECT_EQ("\"Lp2bjAtLP341hvGmYHhxjYyBPJ8/BWbu_eylt5f_aGtCN6mGRv9hABC\"",
+ changelist->etag());
+ EXPECT_EQ("8929", changelist->next_page_token());
+ EXPECT_EQ("https://www.googleapis.com/drive/v2/changes?pageToken=8929",
+ changelist->next_link().spec());
+ EXPECT_EQ(13664, changelist->largest_change_id());
+
+ ASSERT_EQ(4U, changelist->items().size());
+
+ const ChangeResource& change1 = *changelist->items()[0];
+ EXPECT_EQ(8421, change1.change_id());
+ EXPECT_FALSE(change1.is_deleted());
+ EXPECT_EQ("1Pc8jzfU1ErbN_eucMMqdqzY3eBm0v8sxXm_1CtLxABC", change1.file_id());
+ EXPECT_EQ(change1.file_id(), change1.file()->file_id());
+ EXPECT_FALSE(change1.file()->shared());
+
+ const ChangeResource& change2 = *changelist->items()[1];
+ EXPECT_EQ(8424, change2.change_id());
+ EXPECT_FALSE(change2.is_deleted());
+ EXPECT_EQ("0B4v7G8yEYAWHUmRrU2lMS2hLABC", change2.file_id());
+ EXPECT_EQ(change2.file_id(), change2.file()->file_id());
+ EXPECT_TRUE(change2.file()->shared());
+
+ const ChangeResource& change3 = *changelist->items()[2];
+ EXPECT_EQ(8429, change3.change_id());
+ EXPECT_FALSE(change3.is_deleted());
+ EXPECT_EQ("0B4v7G8yEYAWHYW1OcExsUVZLABC", change3.file_id());
+ EXPECT_EQ(change3.file_id(), change3.file()->file_id());
+ EXPECT_FALSE(change3.file()->shared());
+
+ // Deleted entry.
+ const ChangeResource& change4 = *changelist->items()[3];
+ EXPECT_EQ(8430, change4.change_id());
+ EXPECT_EQ("ABCv7G8yEYAWHc3Y5X0hMSkJYXYZ", change4.file_id());
+ EXPECT_TRUE(change4.is_deleted());
+}
+
+TEST(DriveAPIParserTest, HasKind) {
+ scoped_ptr<base::Value> change_list_json(
+ test_util::LoadJSONFile("drive/changelist.json"));
+ scoped_ptr<base::Value> file_list_json(
+ test_util::LoadJSONFile("drive/filelist.json"));
+
+ EXPECT_TRUE(ChangeList::HasChangeListKind(*change_list_json));
+ EXPECT_FALSE(ChangeList::HasChangeListKind(*file_list_json));
+
+ EXPECT_FALSE(FileList::HasFileListKind(*change_list_json));
+ EXPECT_TRUE(FileList::HasFileListKind(*file_list_json));
+}
+
+} // namespace google_apis
diff --git a/chromium/google_apis/drive/drive_api_requests.cc b/chromium/google_apis/drive/drive_api_requests.cc
new file mode 100644
index 00000000000..f0960e6f237
--- /dev/null
+++ b/chromium/google_apis/drive/drive_api_requests.cc
@@ -0,0 +1,743 @@
+// Copyright (c) 2012 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "google_apis/drive/drive_api_requests.h"
+
+#include "base/bind.h"
+#include "base/callback.h"
+#include "base/json/json_writer.h"
+#include "base/location.h"
+#include "base/sequenced_task_runner.h"
+#include "base/task_runner_util.h"
+#include "base/values.h"
+#include "google_apis/drive/drive_api_parser.h"
+#include "google_apis/drive/request_sender.h"
+#include "google_apis/drive/request_util.h"
+#include "google_apis/drive/time_util.h"
+#include "net/base/url_util.h"
+
+namespace google_apis {
+namespace {
+
+const char kContentTypeApplicationJson[] = "application/json";
+const char kParentLinkKind[] = "drive#fileLink";
+
+// Parses the JSON value to a resource typed |T| and runs |callback| on the UI
+// thread once parsing is done.
+template<typename T>
+void ParseJsonAndRun(
+ const base::Callback<void(GDataErrorCode, scoped_ptr<T>)>& callback,
+ GDataErrorCode error,
+ scoped_ptr<base::Value> value) {
+ DCHECK(!callback.is_null());
+
+ scoped_ptr<T> resource;
+ if (value) {
+ resource = T::CreateFrom(*value);
+ if (!resource) {
+ // Failed to parse the JSON value, although the JSON value is available,
+ // so let the callback know the parsing error.
+ error = GDATA_PARSE_ERROR;
+ }
+ }
+
+ callback.Run(error, resource.Pass());
+}
+
+// Thin adapter of T::CreateFrom.
+template<typename T>
+scoped_ptr<T> ParseJsonOnBlockingPool(scoped_ptr<base::Value> value) {
+ return T::CreateFrom(*value);
+}
+
+// Runs |callback| with given |error| and |value|. If |value| is null,
+// overwrites |error| to GDATA_PARSE_ERROR.
+template<typename T>
+void ParseJsonOnBlockingPoolAndRunAfterBlockingPoolTask(
+ const base::Callback<void(GDataErrorCode, scoped_ptr<T>)>& callback,
+ GDataErrorCode error, scoped_ptr<T> value) {
+ if (!value)
+ error = GDATA_PARSE_ERROR;
+ callback.Run(error, value.Pass());
+}
+
+// Parses the JSON value to a resource typed |T| and runs |callback| on
+// blocking pool, and then run on the current thread.
+// TODO(hidehiko): Move this and ParseJsonAndRun defined above into base with
+// merging the tasks running on blocking pool into one.
+template<typename T>
+void ParseJsonOnBlockingPoolAndRun(
+ scoped_refptr<base::TaskRunner> blocking_task_runner,
+ const base::Callback<void(GDataErrorCode, scoped_ptr<T>)>& callback,
+ GDataErrorCode error,
+ scoped_ptr<base::Value> value) {
+ DCHECK(!callback.is_null());
+
+ if (!value) {
+ callback.Run(error, scoped_ptr<T>());
+ return;
+ }
+
+ base::PostTaskAndReplyWithResult(
+ blocking_task_runner,
+ FROM_HERE,
+ base::Bind(&ParseJsonOnBlockingPool<T>, base::Passed(&value)),
+ base::Bind(&ParseJsonOnBlockingPoolAndRunAfterBlockingPoolTask<T>,
+ callback, error));
+}
+
+// Parses the JSON value to FileResource instance and runs |callback| on the
+// UI thread once parsing is done.
+// This is customized version of ParseJsonAndRun defined above to adapt the
+// remaining response type.
+void ParseFileResourceWithUploadRangeAndRun(
+ const drive::UploadRangeCallback& callback,
+ const UploadRangeResponse& response,
+ scoped_ptr<base::Value> value) {
+ DCHECK(!callback.is_null());
+
+ scoped_ptr<FileResource> file_resource;
+ if (value) {
+ file_resource = FileResource::CreateFrom(*value);
+ if (!file_resource) {
+ callback.Run(
+ UploadRangeResponse(GDATA_PARSE_ERROR,
+ response.start_position_received,
+ response.end_position_received),
+ scoped_ptr<FileResource>());
+ return;
+ }
+ }
+
+ callback.Run(response, file_resource.Pass());
+}
+
+} // namespace
+
+namespace drive {
+
+//============================ DriveApiDataRequest ===========================
+
+DriveApiDataRequest::DriveApiDataRequest(RequestSender* sender,
+ const GetDataCallback& callback)
+ : GetDataRequest(sender, callback) {
+}
+
+DriveApiDataRequest::~DriveApiDataRequest() {
+}
+
+GURL DriveApiDataRequest::GetURL() const {
+ GURL url = GetURLInternal();
+ if (!fields_.empty())
+ url = net::AppendOrReplaceQueryParameter(url, "fields", fields_);
+ return url;
+}
+
+//=============================== FilesGetRequest =============================
+
+FilesGetRequest::FilesGetRequest(
+ RequestSender* sender,
+ const DriveApiUrlGenerator& url_generator,
+ const FileResourceCallback& callback)
+ : DriveApiDataRequest(
+ sender,
+ base::Bind(&ParseJsonAndRun<FileResource>, callback)),
+ url_generator_(url_generator) {
+ DCHECK(!callback.is_null());
+}
+
+FilesGetRequest::~FilesGetRequest() {}
+
+GURL FilesGetRequest::GetURLInternal() const {
+ return url_generator_.GetFilesGetUrl(file_id_);
+}
+
+//============================ FilesInsertRequest ============================
+
+FilesInsertRequest::FilesInsertRequest(
+ RequestSender* sender,
+ const DriveApiUrlGenerator& url_generator,
+ const FileResourceCallback& callback)
+ : DriveApiDataRequest(
+ sender,
+ base::Bind(&ParseJsonAndRun<FileResource>, callback)),
+ url_generator_(url_generator) {
+ DCHECK(!callback.is_null());
+}
+
+FilesInsertRequest::~FilesInsertRequest() {}
+
+net::URLFetcher::RequestType FilesInsertRequest::GetRequestType() const {
+ return net::URLFetcher::POST;
+}
+
+bool FilesInsertRequest::GetContentData(std::string* upload_content_type,
+ std::string* upload_content) {
+ *upload_content_type = kContentTypeApplicationJson;
+
+ base::DictionaryValue root;
+
+ if (!mime_type_.empty())
+ root.SetString("mimeType", mime_type_);
+
+ if (!parents_.empty()) {
+ base::ListValue* parents_value = new base::ListValue;
+ for (size_t i = 0; i < parents_.size(); ++i) {
+ base::DictionaryValue* parent = new base::DictionaryValue;
+ parent->SetString("id", parents_[i]);
+ parents_value->Append(parent);
+ }
+ root.Set("parents", parents_value);
+ }
+
+ if (!title_.empty())
+ root.SetString("title", title_);
+
+ base::JSONWriter::Write(&root, upload_content);
+ DVLOG(1) << "FilesInsert data: " << *upload_content_type << ", ["
+ << *upload_content << "]";
+ return true;
+}
+
+GURL FilesInsertRequest::GetURLInternal() const {
+ return url_generator_.GetFilesInsertUrl();
+}
+
+//============================== FilesPatchRequest ============================
+
+FilesPatchRequest::FilesPatchRequest(
+ RequestSender* sender,
+ const DriveApiUrlGenerator& url_generator,
+ const FileResourceCallback& callback)
+ : DriveApiDataRequest(
+ sender,
+ base::Bind(&ParseJsonAndRun<FileResource>, callback)),
+ url_generator_(url_generator),
+ set_modified_date_(false),
+ update_viewed_date_(true) {
+ DCHECK(!callback.is_null());
+}
+
+FilesPatchRequest::~FilesPatchRequest() {}
+
+net::URLFetcher::RequestType FilesPatchRequest::GetRequestType() const {
+ return net::URLFetcher::PATCH;
+}
+
+std::vector<std::string> FilesPatchRequest::GetExtraRequestHeaders() const {
+ std::vector<std::string> headers;
+ headers.push_back(util::kIfMatchAllHeader);
+ return headers;
+}
+
+GURL FilesPatchRequest::GetURLInternal() const {
+ return url_generator_.GetFilesPatchUrl(
+ file_id_, set_modified_date_, update_viewed_date_);
+}
+
+bool FilesPatchRequest::GetContentData(std::string* upload_content_type,
+ std::string* upload_content) {
+ if (title_.empty() &&
+ modified_date_.is_null() &&
+ last_viewed_by_me_date_.is_null() &&
+ parents_.empty())
+ return false;
+
+ *upload_content_type = kContentTypeApplicationJson;
+
+ base::DictionaryValue root;
+ if (!title_.empty())
+ root.SetString("title", title_);
+
+ if (!modified_date_.is_null())
+ root.SetString("modifiedDate", util::FormatTimeAsString(modified_date_));
+
+ if (!last_viewed_by_me_date_.is_null()) {
+ root.SetString("lastViewedByMeDate",
+ util::FormatTimeAsString(last_viewed_by_me_date_));
+ }
+
+ if (!parents_.empty()) {
+ base::ListValue* parents_value = new base::ListValue;
+ for (size_t i = 0; i < parents_.size(); ++i) {
+ base::DictionaryValue* parent = new base::DictionaryValue;
+ parent->SetString("id", parents_[i]);
+ parents_value->Append(parent);
+ }
+ root.Set("parents", parents_value);
+ }
+
+ base::JSONWriter::Write(&root, upload_content);
+ DVLOG(1) << "FilesPatch data: " << *upload_content_type << ", ["
+ << *upload_content << "]";
+ return true;
+}
+
+//============================= FilesCopyRequest ==============================
+
+FilesCopyRequest::FilesCopyRequest(
+ RequestSender* sender,
+ const DriveApiUrlGenerator& url_generator,
+ const FileResourceCallback& callback)
+ : DriveApiDataRequest(
+ sender,
+ base::Bind(&ParseJsonAndRun<FileResource>, callback)),
+ url_generator_(url_generator) {
+ DCHECK(!callback.is_null());
+}
+
+FilesCopyRequest::~FilesCopyRequest() {
+}
+
+net::URLFetcher::RequestType FilesCopyRequest::GetRequestType() const {
+ return net::URLFetcher::POST;
+}
+
+GURL FilesCopyRequest::GetURLInternal() const {
+ return url_generator_.GetFilesCopyUrl(file_id_);
+}
+
+bool FilesCopyRequest::GetContentData(std::string* upload_content_type,
+ std::string* upload_content) {
+ if (parents_.empty() && title_.empty())
+ return false;
+
+ *upload_content_type = kContentTypeApplicationJson;
+
+ base::DictionaryValue root;
+
+ if (!modified_date_.is_null())
+ root.SetString("modifiedDate", util::FormatTimeAsString(modified_date_));
+
+ if (!parents_.empty()) {
+ base::ListValue* parents_value = new base::ListValue;
+ for (size_t i = 0; i < parents_.size(); ++i) {
+ base::DictionaryValue* parent = new base::DictionaryValue;
+ parent->SetString("id", parents_[i]);
+ parents_value->Append(parent);
+ }
+ root.Set("parents", parents_value);
+ }
+
+ if (!title_.empty())
+ root.SetString("title", title_);
+
+ base::JSONWriter::Write(&root, upload_content);
+ DVLOG(1) << "FilesCopy data: " << *upload_content_type << ", ["
+ << *upload_content << "]";
+ return true;
+}
+
+//============================= FilesListRequest =============================
+
+FilesListRequest::FilesListRequest(
+ RequestSender* sender,
+ const DriveApiUrlGenerator& url_generator,
+ const FileListCallback& callback)
+ : DriveApiDataRequest(
+ sender,
+ base::Bind(&ParseJsonOnBlockingPoolAndRun<FileList>,
+ make_scoped_refptr(sender->blocking_task_runner()),
+ callback)),
+ url_generator_(url_generator),
+ max_results_(100) {
+ DCHECK(!callback.is_null());
+}
+
+FilesListRequest::~FilesListRequest() {}
+
+GURL FilesListRequest::GetURLInternal() const {
+ return url_generator_.GetFilesListUrl(max_results_, page_token_, q_);
+}
+
+//======================== FilesListNextPageRequest =========================
+
+FilesListNextPageRequest::FilesListNextPageRequest(
+ RequestSender* sender,
+ const FileListCallback& callback)
+ : DriveApiDataRequest(
+ sender,
+ base::Bind(&ParseJsonOnBlockingPoolAndRun<FileList>,
+ make_scoped_refptr(sender->blocking_task_runner()),
+ callback)) {
+ DCHECK(!callback.is_null());
+}
+
+FilesListNextPageRequest::~FilesListNextPageRequest() {
+}
+
+GURL FilesListNextPageRequest::GetURLInternal() const {
+ return next_link_;
+}
+
+//============================ FilesDeleteRequest =============================
+
+FilesDeleteRequest::FilesDeleteRequest(
+ RequestSender* sender,
+ const DriveApiUrlGenerator& url_generator,
+ const EntryActionCallback& callback)
+ : EntryActionRequest(sender, callback),
+ url_generator_(url_generator) {
+ DCHECK(!callback.is_null());
+}
+
+FilesDeleteRequest::~FilesDeleteRequest() {}
+
+net::URLFetcher::RequestType FilesDeleteRequest::GetRequestType() const {
+ return net::URLFetcher::DELETE_REQUEST;
+}
+
+GURL FilesDeleteRequest::GetURL() const {
+ return url_generator_.GetFilesDeleteUrl(file_id_);
+}
+
+std::vector<std::string> FilesDeleteRequest::GetExtraRequestHeaders() const {
+ std::vector<std::string> headers(
+ EntryActionRequest::GetExtraRequestHeaders());
+ headers.push_back(util::GenerateIfMatchHeader(etag_));
+ return headers;
+}
+
+//============================ FilesTrashRequest =============================
+
+FilesTrashRequest::FilesTrashRequest(
+ RequestSender* sender,
+ const DriveApiUrlGenerator& url_generator,
+ const FileResourceCallback& callback)
+ : DriveApiDataRequest(
+ sender,
+ base::Bind(&ParseJsonAndRun<FileResource>, callback)),
+ url_generator_(url_generator) {
+ DCHECK(!callback.is_null());
+}
+
+FilesTrashRequest::~FilesTrashRequest() {}
+
+net::URLFetcher::RequestType FilesTrashRequest::GetRequestType() const {
+ return net::URLFetcher::POST;
+}
+
+GURL FilesTrashRequest::GetURLInternal() const {
+ return url_generator_.GetFilesTrashUrl(file_id_);
+}
+
+//============================== AboutGetRequest =============================
+
+AboutGetRequest::AboutGetRequest(
+ RequestSender* sender,
+ const DriveApiUrlGenerator& url_generator,
+ const AboutResourceCallback& callback)
+ : DriveApiDataRequest(
+ sender,
+ base::Bind(&ParseJsonAndRun<AboutResource>, callback)),
+ url_generator_(url_generator) {
+ DCHECK(!callback.is_null());
+}
+
+AboutGetRequest::~AboutGetRequest() {}
+
+GURL AboutGetRequest::GetURLInternal() const {
+ return url_generator_.GetAboutGetUrl();
+}
+
+//============================ ChangesListRequest ===========================
+
+ChangesListRequest::ChangesListRequest(
+ RequestSender* sender,
+ const DriveApiUrlGenerator& url_generator,
+ const ChangeListCallback& callback)
+ : DriveApiDataRequest(
+ sender,
+ base::Bind(&ParseJsonOnBlockingPoolAndRun<ChangeList>,
+ make_scoped_refptr(sender->blocking_task_runner()),
+ callback)),
+ url_generator_(url_generator),
+ include_deleted_(true),
+ max_results_(100),
+ start_change_id_(0) {
+ DCHECK(!callback.is_null());
+}
+
+ChangesListRequest::~ChangesListRequest() {}
+
+GURL ChangesListRequest::GetURLInternal() const {
+ return url_generator_.GetChangesListUrl(
+ include_deleted_, max_results_, page_token_, start_change_id_);
+}
+
+//======================== ChangesListNextPageRequest =========================
+
+ChangesListNextPageRequest::ChangesListNextPageRequest(
+ RequestSender* sender,
+ const ChangeListCallback& callback)
+ : DriveApiDataRequest(
+ sender,
+ base::Bind(&ParseJsonOnBlockingPoolAndRun<ChangeList>,
+ make_scoped_refptr(sender->blocking_task_runner()),
+ callback)) {
+ DCHECK(!callback.is_null());
+}
+
+ChangesListNextPageRequest::~ChangesListNextPageRequest() {
+}
+
+GURL ChangesListNextPageRequest::GetURLInternal() const {
+ return next_link_;
+}
+
+//============================== AppsListRequest ===========================
+
+AppsListRequest::AppsListRequest(
+ RequestSender* sender,
+ const DriveApiUrlGenerator& url_generator,
+ const AppListCallback& callback)
+ : DriveApiDataRequest(
+ sender,
+ base::Bind(&ParseJsonAndRun<AppList>, callback)),
+ url_generator_(url_generator) {
+ DCHECK(!callback.is_null());
+}
+
+AppsListRequest::~AppsListRequest() {}
+
+GURL AppsListRequest::GetURLInternal() const {
+ return url_generator_.GetAppsListUrl();
+}
+
+//========================== ChildrenInsertRequest ============================
+
+ChildrenInsertRequest::ChildrenInsertRequest(
+ RequestSender* sender,
+ const DriveApiUrlGenerator& url_generator,
+ const EntryActionCallback& callback)
+ : EntryActionRequest(sender, callback),
+ url_generator_(url_generator) {
+ DCHECK(!callback.is_null());
+}
+
+ChildrenInsertRequest::~ChildrenInsertRequest() {}
+
+net::URLFetcher::RequestType ChildrenInsertRequest::GetRequestType() const {
+ return net::URLFetcher::POST;
+}
+
+GURL ChildrenInsertRequest::GetURL() const {
+ return url_generator_.GetChildrenInsertUrl(folder_id_);
+}
+
+bool ChildrenInsertRequest::GetContentData(std::string* upload_content_type,
+ std::string* upload_content) {
+ *upload_content_type = kContentTypeApplicationJson;
+
+ base::DictionaryValue root;
+ root.SetString("id", id_);
+
+ base::JSONWriter::Write(&root, upload_content);
+ DVLOG(1) << "InsertResource data: " << *upload_content_type << ", ["
+ << *upload_content << "]";
+ return true;
+}
+
+//========================== ChildrenDeleteRequest ============================
+
+ChildrenDeleteRequest::ChildrenDeleteRequest(
+ RequestSender* sender,
+ const DriveApiUrlGenerator& url_generator,
+ const EntryActionCallback& callback)
+ : EntryActionRequest(sender, callback),
+ url_generator_(url_generator) {
+ DCHECK(!callback.is_null());
+}
+
+ChildrenDeleteRequest::~ChildrenDeleteRequest() {}
+
+net::URLFetcher::RequestType ChildrenDeleteRequest::GetRequestType() const {
+ return net::URLFetcher::DELETE_REQUEST;
+}
+
+GURL ChildrenDeleteRequest::GetURL() const {
+ return url_generator_.GetChildrenDeleteUrl(child_id_, folder_id_);
+}
+
+//======================= InitiateUploadNewFileRequest =======================
+
+InitiateUploadNewFileRequest::InitiateUploadNewFileRequest(
+ RequestSender* sender,
+ const DriveApiUrlGenerator& url_generator,
+ const std::string& content_type,
+ int64 content_length,
+ const std::string& parent_resource_id,
+ const std::string& title,
+ const InitiateUploadCallback& callback)
+ : InitiateUploadRequestBase(sender,
+ callback,
+ content_type,
+ content_length),
+ url_generator_(url_generator),
+ parent_resource_id_(parent_resource_id),
+ title_(title) {
+}
+
+InitiateUploadNewFileRequest::~InitiateUploadNewFileRequest() {}
+
+GURL InitiateUploadNewFileRequest::GetURL() const {
+ return url_generator_.GetInitiateUploadNewFileUrl();
+}
+
+net::URLFetcher::RequestType
+InitiateUploadNewFileRequest::GetRequestType() const {
+ return net::URLFetcher::POST;
+}
+
+bool InitiateUploadNewFileRequest::GetContentData(
+ std::string* upload_content_type,
+ std::string* upload_content) {
+ *upload_content_type = kContentTypeApplicationJson;
+
+ base::DictionaryValue root;
+ root.SetString("title", title_);
+
+ // Fill parent link.
+ {
+ scoped_ptr<base::DictionaryValue> parent(new base::DictionaryValue);
+ parent->SetString("kind", kParentLinkKind);
+ parent->SetString("id", parent_resource_id_);
+
+ scoped_ptr<base::ListValue> parents(new base::ListValue);
+ parents->Append(parent.release());
+
+ root.Set("parents", parents.release());
+ }
+
+ base::JSONWriter::Write(&root, upload_content);
+
+ DVLOG(1) << "InitiateUploadNewFile data: " << *upload_content_type << ", ["
+ << *upload_content << "]";
+ return true;
+}
+
+//===================== InitiateUploadExistingFileRequest ====================
+
+InitiateUploadExistingFileRequest::InitiateUploadExistingFileRequest(
+ RequestSender* sender,
+ const DriveApiUrlGenerator& url_generator,
+ const std::string& content_type,
+ int64 content_length,
+ const std::string& resource_id,
+ const std::string& etag,
+ const InitiateUploadCallback& callback)
+ : InitiateUploadRequestBase(sender,
+ callback,
+ content_type,
+ content_length),
+ url_generator_(url_generator),
+ resource_id_(resource_id),
+ etag_(etag) {
+}
+
+InitiateUploadExistingFileRequest::~InitiateUploadExistingFileRequest() {}
+
+GURL InitiateUploadExistingFileRequest::GetURL() const {
+ return url_generator_.GetInitiateUploadExistingFileUrl(resource_id_);
+}
+
+net::URLFetcher::RequestType
+InitiateUploadExistingFileRequest::GetRequestType() const {
+ return net::URLFetcher::PUT;
+}
+
+std::vector<std::string>
+InitiateUploadExistingFileRequest::GetExtraRequestHeaders() const {
+ std::vector<std::string> headers(
+ InitiateUploadRequestBase::GetExtraRequestHeaders());
+ headers.push_back(util::GenerateIfMatchHeader(etag_));
+ return headers;
+}
+
+//============================ ResumeUploadRequest ===========================
+
+ResumeUploadRequest::ResumeUploadRequest(
+ RequestSender* sender,
+ const GURL& upload_location,
+ int64 start_position,
+ int64 end_position,
+ int64 content_length,
+ const std::string& content_type,
+ const base::FilePath& local_file_path,
+ const UploadRangeCallback& callback,
+ const ProgressCallback& progress_callback)
+ : ResumeUploadRequestBase(sender,
+ upload_location,
+ start_position,
+ end_position,
+ content_length,
+ content_type,
+ local_file_path),
+ callback_(callback),
+ progress_callback_(progress_callback) {
+ DCHECK(!callback_.is_null());
+}
+
+ResumeUploadRequest::~ResumeUploadRequest() {}
+
+void ResumeUploadRequest::OnRangeRequestComplete(
+ const UploadRangeResponse& response,
+ scoped_ptr<base::Value> value) {
+ DCHECK(CalledOnValidThread());
+ ParseFileResourceWithUploadRangeAndRun(callback_, response, value.Pass());
+}
+
+void ResumeUploadRequest::OnURLFetchUploadProgress(
+ const net::URLFetcher* source, int64 current, int64 total) {
+ if (!progress_callback_.is_null())
+ progress_callback_.Run(current, total);
+}
+
+//========================== GetUploadStatusRequest ==========================
+
+GetUploadStatusRequest::GetUploadStatusRequest(
+ RequestSender* sender,
+ const GURL& upload_url,
+ int64 content_length,
+ const UploadRangeCallback& callback)
+ : GetUploadStatusRequestBase(sender,
+ upload_url,
+ content_length),
+ callback_(callback) {
+ DCHECK(!callback.is_null());
+}
+
+GetUploadStatusRequest::~GetUploadStatusRequest() {}
+
+void GetUploadStatusRequest::OnRangeRequestComplete(
+ const UploadRangeResponse& response,
+ scoped_ptr<base::Value> value) {
+ DCHECK(CalledOnValidThread());
+ ParseFileResourceWithUploadRangeAndRun(callback_, response, value.Pass());
+}
+
+//========================== DownloadFileRequest ==========================
+
+DownloadFileRequest::DownloadFileRequest(
+ RequestSender* sender,
+ const DriveApiUrlGenerator& url_generator,
+ const std::string& resource_id,
+ const base::FilePath& output_file_path,
+ const DownloadActionCallback& download_action_callback,
+ const GetContentCallback& get_content_callback,
+ const ProgressCallback& progress_callback)
+ : DownloadFileRequestBase(
+ sender,
+ download_action_callback,
+ get_content_callback,
+ progress_callback,
+ url_generator.GenerateDownloadFileUrl(resource_id),
+ output_file_path) {
+}
+
+DownloadFileRequest::~DownloadFileRequest() {
+}
+
+} // namespace drive
+} // namespace google_apis
diff --git a/chromium/google_apis/drive/drive_api_requests.h b/chromium/google_apis/drive/drive_api_requests.h
new file mode 100644
index 00000000000..6f10aa66d95
--- /dev/null
+++ b/chromium/google_apis/drive/drive_api_requests.h
@@ -0,0 +1,733 @@
+// Copyright (c) 2012 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef GOOGLE_APIS_DRIVE_DRIVE_API_REQUESTS_H_
+#define GOOGLE_APIS_DRIVE_DRIVE_API_REQUESTS_H_
+
+#include <string>
+
+#include "base/callback_forward.h"
+#include "base/time/time.h"
+#include "google_apis/drive/base_requests.h"
+#include "google_apis/drive/drive_api_url_generator.h"
+#include "google_apis/drive/drive_common_callbacks.h"
+
+namespace google_apis {
+
+class ChangeList;
+class FileResource;
+class FileList;
+
+// Callback used for requests that the server returns FileResource data
+// formatted into JSON value.
+typedef base::Callback<void(GDataErrorCode error,
+ scoped_ptr<FileResource> entry)>
+ FileResourceCallback;
+
+// Callback used for requests that the server returns FileList data
+// formatted into JSON value.
+typedef base::Callback<void(GDataErrorCode error,
+ scoped_ptr<FileList> entry)> FileListCallback;
+
+// Callback used for requests that the server returns ChangeList data
+// formatted into JSON value.
+typedef base::Callback<void(GDataErrorCode error,
+ scoped_ptr<ChangeList> entry)> ChangeListCallback;
+
+namespace drive {
+
+//============================ DriveApiDataRequest ===========================
+
+// This is base class of the Drive API related requests. All Drive API requests
+// support partial request (to improve the performance). The function can be
+// shared among the Drive API requests.
+// See also https://developers.google.com/drive/performance
+class DriveApiDataRequest : public GetDataRequest {
+ public:
+ DriveApiDataRequest(RequestSender* sender, const GetDataCallback& callback);
+ virtual ~DriveApiDataRequest();
+
+ // Optional parameter.
+ const std::string& fields() const { return fields_; }
+ void set_fields(const std::string& fields) { fields_ = fields; }
+
+ protected:
+ // Overridden from GetDataRequest.
+ virtual GURL GetURL() const OVERRIDE;
+
+ // Derived classes should override GetURLInternal instead of GetURL()
+ // directly.
+ virtual GURL GetURLInternal() const = 0;
+
+ private:
+ std::string fields_;
+
+ DISALLOW_COPY_AND_ASSIGN(DriveApiDataRequest);
+};
+
+//=============================== FilesGetRequest =============================
+
+// This class performs the request for fetching a file.
+// This request is mapped to
+// https://developers.google.com/drive/v2/reference/files/get
+class FilesGetRequest : public DriveApiDataRequest {
+ public:
+ FilesGetRequest(RequestSender* sender,
+ const DriveApiUrlGenerator& url_generator,
+ const FileResourceCallback& callback);
+ virtual ~FilesGetRequest();
+
+ // Required parameter.
+ const std::string& file_id() const { return file_id_; }
+ void set_file_id(const std::string& file_id) { file_id_ = file_id; }
+
+ protected:
+ // Overridden from DriveApiDataRequest.
+ virtual GURL GetURLInternal() const OVERRIDE;
+
+ private:
+ const DriveApiUrlGenerator url_generator_;
+ std::string file_id_;
+
+ DISALLOW_COPY_AND_ASSIGN(FilesGetRequest);
+};
+
+//============================ FilesInsertRequest =============================
+
+// This class performs the request for creating a resource.
+// This request is mapped to
+// https://developers.google.com/drive/v2/reference/files/insert
+// See also https://developers.google.com/drive/manage-uploads and
+// https://developers.google.com/drive/folder
+class FilesInsertRequest : public DriveApiDataRequest {
+ public:
+ FilesInsertRequest(RequestSender* sender,
+ const DriveApiUrlGenerator& url_generator,
+ const FileResourceCallback& callback);
+ virtual ~FilesInsertRequest();
+
+ // Optional request body.
+ const std::string& mime_type() const { return mime_type_; }
+ void set_mime_type(const std::string& mime_type) {
+ mime_type_ = mime_type;
+ }
+
+ const std::vector<std::string>& parents() const { return parents_; }
+ void add_parent(const std::string& parent) { parents_.push_back(parent); }
+
+ const std::string& title() const { return title_; }
+ void set_title(const std::string& title) { title_ = title; }
+
+ protected:
+ // Overridden from GetDataRequest.
+ virtual net::URLFetcher::RequestType GetRequestType() const OVERRIDE;
+ virtual bool GetContentData(std::string* upload_content_type,
+ std::string* upload_content) OVERRIDE;
+
+ // Overridden from DriveApiDataRequest.
+ virtual GURL GetURLInternal() const OVERRIDE;
+
+ private:
+ const DriveApiUrlGenerator url_generator_;
+
+ std::string mime_type_;
+ std::vector<std::string> parents_;
+ std::string title_;
+
+ DISALLOW_COPY_AND_ASSIGN(FilesInsertRequest);
+};
+
+//============================== FilesPatchRequest ============================
+
+// This class performs the request for patching file metadata.
+// This request is mapped to
+// https://developers.google.com/drive/v2/reference/files/patch
+class FilesPatchRequest : public DriveApiDataRequest {
+ public:
+ FilesPatchRequest(RequestSender* sender,
+ const DriveApiUrlGenerator& url_generator,
+ const FileResourceCallback& callback);
+ virtual ~FilesPatchRequest();
+
+ // Required parameter.
+ const std::string& file_id() const { return file_id_; }
+ void set_file_id(const std::string& file_id) { file_id_ = file_id; }
+
+ // Optional parameter.
+ bool set_modified_date() const { return set_modified_date_; }
+ void set_set_modified_date(bool set_modified_date) {
+ set_modified_date_ = set_modified_date;
+ }
+
+ bool update_viewed_date() const { return update_viewed_date_; }
+ void set_update_viewed_date(bool update_viewed_date) {
+ update_viewed_date_ = update_viewed_date;
+ }
+
+ // Optional request body.
+ // Note: "Files: patch" accepts any "Files resource" data, but this class
+ // only supports limited members of it for now. We can extend it upon
+ // requirments.
+ const std::string& title() const { return title_; }
+ void set_title(const std::string& title) { title_ = title; }
+
+ const base::Time& modified_date() const { return modified_date_; }
+ void set_modified_date(const base::Time& modified_date) {
+ modified_date_ = modified_date;
+ }
+
+ const base::Time& last_viewed_by_me_date() const {
+ return last_viewed_by_me_date_;
+ }
+ void set_last_viewed_by_me_date(const base::Time& last_viewed_by_me_date) {
+ last_viewed_by_me_date_ = last_viewed_by_me_date;
+ }
+
+ const std::vector<std::string>& parents() const { return parents_; }
+ void add_parent(const std::string& parent) { parents_.push_back(parent); }
+
+ protected:
+ // Overridden from URLFetchRequestBase.
+ virtual net::URLFetcher::RequestType GetRequestType() const OVERRIDE;
+ virtual std::vector<std::string> GetExtraRequestHeaders() const OVERRIDE;
+ virtual bool GetContentData(std::string* upload_content_type,
+ std::string* upload_content) OVERRIDE;
+
+ // Overridden from DriveApiDataRequest.
+ virtual GURL GetURLInternal() const OVERRIDE;
+
+ private:
+ const DriveApiUrlGenerator url_generator_;
+
+ std::string file_id_;
+ bool set_modified_date_;
+ bool update_viewed_date_;
+
+ std::string title_;
+ base::Time modified_date_;
+ base::Time last_viewed_by_me_date_;
+ std::vector<std::string> parents_;
+
+ DISALLOW_COPY_AND_ASSIGN(FilesPatchRequest);
+};
+
+//============================= FilesCopyRequest ==============================
+
+// This class performs the request for copying a resource.
+// This request is mapped to
+// https://developers.google.com/drive/v2/reference/files/copy
+class FilesCopyRequest : public DriveApiDataRequest {
+ public:
+ // Upon completion, |callback| will be called. |callback| must not be null.
+ FilesCopyRequest(RequestSender* sender,
+ const DriveApiUrlGenerator& url_generator,
+ const FileResourceCallback& callback);
+ virtual ~FilesCopyRequest();
+
+ // Required parameter.
+ const std::string& file_id() const { return file_id_; }
+ void set_file_id(const std::string& file_id) { file_id_ = file_id; }
+
+ // Optional request body.
+ const std::vector<std::string>& parents() const { return parents_; }
+ void add_parent(const std::string& parent) { parents_.push_back(parent); }
+
+ const base::Time& modified_date() const { return modified_date_; }
+ void set_modified_date(const base::Time& modified_date) {
+ modified_date_ = modified_date;
+ }
+
+ const std::string& title() const { return title_; }
+ void set_title(const std::string& title) { title_ = title; }
+
+ protected:
+ // Overridden from URLFetchRequestBase.
+ virtual net::URLFetcher::RequestType GetRequestType() const OVERRIDE;
+ virtual bool GetContentData(std::string* upload_content_type,
+ std::string* upload_content) OVERRIDE;
+
+ // Overridden from DriveApiDataRequest.
+ virtual GURL GetURLInternal() const OVERRIDE;
+
+ private:
+ const DriveApiUrlGenerator url_generator_;
+
+ std::string file_id_;
+ base::Time modified_date_;
+ std::vector<std::string> parents_;
+ std::string title_;
+
+ DISALLOW_COPY_AND_ASSIGN(FilesCopyRequest);
+};
+
+//============================= FilesListRequest =============================
+
+// This class performs the request for fetching FileList.
+// The result may contain only first part of the result. The remaining result
+// should be able to be fetched by ContinueGetFileListRequest defined below,
+// or by FilesListRequest with setting page token.
+// This request is mapped to
+// https://developers.google.com/drive/v2/reference/files/list
+class FilesListRequest : public DriveApiDataRequest {
+ public:
+ FilesListRequest(RequestSender* sender,
+ const DriveApiUrlGenerator& url_generator,
+ const FileListCallback& callback);
+ virtual ~FilesListRequest();
+
+ // Optional parameter
+ int max_results() const { return max_results_; }
+ void set_max_results(int max_results) { max_results_ = max_results; }
+
+ const std::string& page_token() const { return page_token_; }
+ void set_page_token(const std::string& page_token) {
+ page_token_ = page_token;
+ }
+
+ const std::string& q() const { return q_; }
+ void set_q(const std::string& q) { q_ = q; }
+
+ protected:
+ // Overridden from DriveApiDataRequest.
+ virtual GURL GetURLInternal() const OVERRIDE;
+
+ private:
+ const DriveApiUrlGenerator url_generator_;
+ int max_results_;
+ std::string page_token_;
+ std::string q_;
+
+ DISALLOW_COPY_AND_ASSIGN(FilesListRequest);
+};
+
+//========================= FilesListNextPageRequest ==========================
+
+// There are two ways to obtain next pages of "Files: list" result (if paged).
+// 1) Set pageToken and all params used for the initial request.
+// 2) Use URL in the nextLink field in the previous response.
+// This class implements 2)'s request.
+class FilesListNextPageRequest : public DriveApiDataRequest {
+ public:
+ FilesListNextPageRequest(RequestSender* sender,
+ const FileListCallback& callback);
+ virtual ~FilesListNextPageRequest();
+
+ const GURL& next_link() const { return next_link_; }
+ void set_next_link(const GURL& next_link) { next_link_ = next_link; }
+
+ protected:
+ // Overridden from DriveApiDataRequest.
+ virtual GURL GetURLInternal() const OVERRIDE;
+
+ private:
+ GURL next_link_;
+
+ DISALLOW_COPY_AND_ASSIGN(FilesListNextPageRequest);
+};
+
+//============================= FilesDeleteRequest =============================
+
+// This class performs the request for deleting a resource.
+// This request is mapped to
+// https://developers.google.com/drive/v2/reference/files/delete
+class FilesDeleteRequest : public EntryActionRequest {
+ public:
+ FilesDeleteRequest(RequestSender* sender,
+ const DriveApiUrlGenerator& url_generator,
+ const EntryActionCallback& callback);
+ virtual ~FilesDeleteRequest();
+
+ // Required parameter.
+ const std::string& file_id() const { return file_id_; }
+ void set_file_id(const std::string& file_id) { file_id_ = file_id; }
+ void set_etag(const std::string& etag) { etag_ = etag; }
+
+ protected:
+ // Overridden from UrlFetchRequestBase.
+ virtual net::URLFetcher::RequestType GetRequestType() const OVERRIDE;
+ virtual GURL GetURL() const OVERRIDE;
+ virtual std::vector<std::string> GetExtraRequestHeaders() const OVERRIDE;
+
+ private:
+ const DriveApiUrlGenerator url_generator_;
+ std::string file_id_;
+ std::string etag_;
+
+ DISALLOW_COPY_AND_ASSIGN(FilesDeleteRequest);
+};
+
+//============================= FilesTrashRequest ==============================
+
+// This class performs the request for trashing a resource.
+// This request is mapped to
+// https://developers.google.com/drive/v2/reference/files/trash
+class FilesTrashRequest : public DriveApiDataRequest {
+ public:
+ FilesTrashRequest(RequestSender* sender,
+ const DriveApiUrlGenerator& url_generator,
+ const FileResourceCallback& callback);
+ virtual ~FilesTrashRequest();
+
+ // Required parameter.
+ const std::string& file_id() const { return file_id_; }
+ void set_file_id(const std::string& file_id) { file_id_ = file_id; }
+
+ protected:
+ // Overridden from UrlFetchRequestBase.
+ virtual net::URLFetcher::RequestType GetRequestType() const OVERRIDE;
+
+ // Overridden from DriveApiDataRequest.
+ virtual GURL GetURLInternal() const OVERRIDE;
+
+ private:
+ const DriveApiUrlGenerator url_generator_;
+ std::string file_id_;
+
+ DISALLOW_COPY_AND_ASSIGN(FilesTrashRequest);
+};
+
+//============================== AboutGetRequest =============================
+
+// This class performs the request for fetching About data.
+// This request is mapped to
+// https://developers.google.com/drive/v2/reference/about/get
+class AboutGetRequest : public DriveApiDataRequest {
+ public:
+ AboutGetRequest(RequestSender* sender,
+ const DriveApiUrlGenerator& url_generator,
+ const AboutResourceCallback& callback);
+ virtual ~AboutGetRequest();
+
+ protected:
+ // Overridden from DriveApiDataRequest.
+ virtual GURL GetURLInternal() const OVERRIDE;
+
+ private:
+ const DriveApiUrlGenerator url_generator_;
+
+ DISALLOW_COPY_AND_ASSIGN(AboutGetRequest);
+};
+
+//============================ ChangesListRequest ============================
+
+// This class performs the request for fetching ChangeList.
+// The result may contain only first part of the result. The remaining result
+// should be able to be fetched by ContinueGetFileListRequest defined below.
+// or by ChangesListRequest with setting page token.
+// This request is mapped to
+// https://developers.google.com/drive/v2/reference/changes/list
+class ChangesListRequest : public DriveApiDataRequest {
+ public:
+ ChangesListRequest(RequestSender* sender,
+ const DriveApiUrlGenerator& url_generator,
+ const ChangeListCallback& callback);
+ virtual ~ChangesListRequest();
+
+ // Optional parameter
+ bool include_deleted() const { return include_deleted_; }
+ void set_include_deleted(bool include_deleted) {
+ include_deleted_ = include_deleted;
+ }
+
+ int max_results() const { return max_results_; }
+ void set_max_results(int max_results) { max_results_ = max_results; }
+
+ const std::string& page_token() const { return page_token_; }
+ void set_page_token(const std::string& page_token) {
+ page_token_ = page_token;
+ }
+
+ int64 start_change_id() const { return start_change_id_; }
+ void set_start_change_id(int64 start_change_id) {
+ start_change_id_ = start_change_id;
+ }
+
+ protected:
+ // Overridden from DriveApiDataRequest.
+ virtual GURL GetURLInternal() const OVERRIDE;
+
+ private:
+ const DriveApiUrlGenerator url_generator_;
+ bool include_deleted_;
+ int max_results_;
+ std::string page_token_;
+ int64 start_change_id_;
+
+ DISALLOW_COPY_AND_ASSIGN(ChangesListRequest);
+};
+
+//======================== ChangesListNextPageRequest =========================
+
+// There are two ways to obtain next pages of "Changes: list" result (if paged).
+// 1) Set pageToken and all params used for the initial request.
+// 2) Use URL in the nextLink field in the previous response.
+// This class implements 2)'s request.
+class ChangesListNextPageRequest : public DriveApiDataRequest {
+ public:
+ ChangesListNextPageRequest(RequestSender* sender,
+ const ChangeListCallback& callback);
+ virtual ~ChangesListNextPageRequest();
+
+ const GURL& next_link() const { return next_link_; }
+ void set_next_link(const GURL& next_link) { next_link_ = next_link; }
+
+ protected:
+ // Overridden from DriveApiDataRequest.
+ virtual GURL GetURLInternal() const OVERRIDE;
+
+ private:
+ GURL next_link_;
+
+ DISALLOW_COPY_AND_ASSIGN(ChangesListNextPageRequest);
+};
+
+//============================= AppsListRequest ============================
+
+// This class performs the request for fetching AppList.
+// This request is mapped to
+// https://developers.google.com/drive/v2/reference/apps/list
+class AppsListRequest : public DriveApiDataRequest {
+ public:
+ AppsListRequest(RequestSender* sender,
+ const DriveApiUrlGenerator& url_generator,
+ const AppListCallback& callback);
+ virtual ~AppsListRequest();
+
+ protected:
+ // Overridden from DriveApiDataRequest.
+ virtual GURL GetURLInternal() const OVERRIDE;
+
+ private:
+ const DriveApiUrlGenerator url_generator_;
+
+ DISALLOW_COPY_AND_ASSIGN(AppsListRequest);
+};
+
+//========================== ChildrenInsertRequest ============================
+
+// This class performs the request for inserting a resource to a directory.
+// This request is mapped to
+// https://developers.google.com/drive/v2/reference/children/insert
+class ChildrenInsertRequest : public EntryActionRequest {
+ public:
+ ChildrenInsertRequest(RequestSender* sender,
+ const DriveApiUrlGenerator& url_generator,
+ const EntryActionCallback& callback);
+ virtual ~ChildrenInsertRequest();
+
+ // Required parameter.
+ const std::string& folder_id() const { return folder_id_; }
+ void set_folder_id(const std::string& folder_id) {
+ folder_id_ = folder_id;
+ }
+
+ // Required body.
+ const std::string& id() const { return id_; }
+ void set_id(const std::string& id) { id_ = id; }
+
+ protected:
+ // UrlFetchRequestBase overrides.
+ virtual net::URLFetcher::RequestType GetRequestType() const OVERRIDE;
+ virtual GURL GetURL() const OVERRIDE;
+ virtual bool GetContentData(std::string* upload_content_type,
+ std::string* upload_content) OVERRIDE;
+
+ private:
+ const DriveApiUrlGenerator url_generator_;
+ std::string folder_id_;
+ std::string id_;
+
+ DISALLOW_COPY_AND_ASSIGN(ChildrenInsertRequest);
+};
+
+//========================== ChildrenDeleteRequest ============================
+
+// This class performs the request for removing a resource from a directory.
+// This request is mapped to
+// https://developers.google.com/drive/v2/reference/children/delete
+class ChildrenDeleteRequest : public EntryActionRequest {
+ public:
+ // |callback| must not be null.
+ ChildrenDeleteRequest(RequestSender* sender,
+ const DriveApiUrlGenerator& url_generator,
+ const EntryActionCallback& callback);
+ virtual ~ChildrenDeleteRequest();
+
+ // Required parameter.
+ const std::string& child_id() const { return child_id_; }
+ void set_child_id(const std::string& child_id) {
+ child_id_ = child_id;
+ }
+
+ const std::string& folder_id() const { return folder_id_; }
+ void set_folder_id(const std::string& folder_id) {
+ folder_id_ = folder_id;
+ }
+
+ protected:
+ // UrlFetchRequestBase overrides.
+ virtual net::URLFetcher::RequestType GetRequestType() const OVERRIDE;
+ virtual GURL GetURL() const OVERRIDE;
+
+ private:
+ const DriveApiUrlGenerator url_generator_;
+ std::string child_id_;
+ std::string folder_id_;
+
+ DISALLOW_COPY_AND_ASSIGN(ChildrenDeleteRequest);
+};
+
+//======================= InitiateUploadNewFileRequest =======================
+
+// This class performs the request for initiating the upload of a new file.
+class InitiateUploadNewFileRequest : public InitiateUploadRequestBase {
+ public:
+ // |parent_resource_id| should be the resource id of the parent directory.
+ // |title| should be set.
+ // See also the comments of InitiateUploadRequestBase for more details
+ // about the other parameters.
+ InitiateUploadNewFileRequest(RequestSender* sender,
+ const DriveApiUrlGenerator& url_generator,
+ const std::string& content_type,
+ int64 content_length,
+ const std::string& parent_resource_id,
+ const std::string& title,
+ const InitiateUploadCallback& callback);
+ virtual ~InitiateUploadNewFileRequest();
+
+ protected:
+ // UrlFetchRequestBase overrides.
+ virtual GURL GetURL() const OVERRIDE;
+ virtual net::URLFetcher::RequestType GetRequestType() const OVERRIDE;
+ virtual bool GetContentData(std::string* upload_content_type,
+ std::string* upload_content) OVERRIDE;
+
+ private:
+ const DriveApiUrlGenerator url_generator_;
+ const std::string parent_resource_id_;
+ const std::string title_;
+
+ DISALLOW_COPY_AND_ASSIGN(InitiateUploadNewFileRequest);
+};
+
+//==================== InitiateUploadExistingFileRequest =====================
+
+// This class performs the request for initiating the upload of an existing
+// file.
+class InitiateUploadExistingFileRequest : public InitiateUploadRequestBase {
+ public:
+ // |upload_url| should be the upload_url() of the file
+ // (resumable-create-media URL)
+ // |etag| should be set if it is available to detect the upload confliction.
+ // See also the comments of InitiateUploadRequestBase for more details
+ // about the other parameters.
+ InitiateUploadExistingFileRequest(RequestSender* sender,
+ const DriveApiUrlGenerator& url_generator,
+ const std::string& content_type,
+ int64 content_length,
+ const std::string& resource_id,
+ const std::string& etag,
+ const InitiateUploadCallback& callback);
+ virtual ~InitiateUploadExistingFileRequest();
+
+ protected:
+ // UrlFetchRequestBase overrides.
+ virtual GURL GetURL() const OVERRIDE;
+ virtual net::URLFetcher::RequestType GetRequestType() const OVERRIDE;
+ virtual std::vector<std::string> GetExtraRequestHeaders() const OVERRIDE;
+
+ private:
+ const DriveApiUrlGenerator url_generator_;
+ const std::string resource_id_;
+ const std::string etag_;
+
+ DISALLOW_COPY_AND_ASSIGN(InitiateUploadExistingFileRequest);
+};
+
+// Callback used for ResumeUpload() and GetUploadStatus().
+typedef base::Callback<void(
+ const UploadRangeResponse& response,
+ scoped_ptr<FileResource> new_resource)> UploadRangeCallback;
+
+//============================ ResumeUploadRequest ===========================
+
+// Performs the request for resuming the upload of a file.
+class ResumeUploadRequest : public ResumeUploadRequestBase {
+ public:
+ // See also ResumeUploadRequestBase's comment for parameters meaning.
+ // |callback| must not be null. |progress_callback| may be null.
+ ResumeUploadRequest(RequestSender* sender,
+ const GURL& upload_location,
+ int64 start_position,
+ int64 end_position,
+ int64 content_length,
+ const std::string& content_type,
+ const base::FilePath& local_file_path,
+ const UploadRangeCallback& callback,
+ const ProgressCallback& progress_callback);
+ virtual ~ResumeUploadRequest();
+
+ protected:
+ // UploadRangeRequestBase overrides.
+ virtual void OnRangeRequestComplete(
+ const UploadRangeResponse& response,
+ scoped_ptr<base::Value> value) OVERRIDE;
+ // content::UrlFetcherDelegate overrides.
+ virtual void OnURLFetchUploadProgress(const net::URLFetcher* source,
+ int64 current, int64 total) OVERRIDE;
+
+ private:
+ const UploadRangeCallback callback_;
+ const ProgressCallback progress_callback_;
+
+ DISALLOW_COPY_AND_ASSIGN(ResumeUploadRequest);
+};
+
+//========================== GetUploadStatusRequest ==========================
+
+// Performs the request to fetch the current upload status of a file.
+class GetUploadStatusRequest : public GetUploadStatusRequestBase {
+ public:
+ // See also GetUploadStatusRequestBase's comment for parameters meaning.
+ // |callback| must not be null.
+ GetUploadStatusRequest(RequestSender* sender,
+ const GURL& upload_url,
+ int64 content_length,
+ const UploadRangeCallback& callback);
+ virtual ~GetUploadStatusRequest();
+
+ protected:
+ // UploadRangeRequestBase overrides.
+ virtual void OnRangeRequestComplete(
+ const UploadRangeResponse& response,
+ scoped_ptr<base::Value> value) OVERRIDE;
+
+ private:
+ const UploadRangeCallback callback_;
+
+ DISALLOW_COPY_AND_ASSIGN(GetUploadStatusRequest);
+};
+
+//========================== DownloadFileRequest ==========================
+
+// This class performs the request for downloading of a specified file.
+class DownloadFileRequest : public DownloadFileRequestBase {
+ public:
+ // See also DownloadFileRequestBase's comment for parameters meaning.
+ DownloadFileRequest(RequestSender* sender,
+ const DriveApiUrlGenerator& url_generator,
+ const std::string& resource_id,
+ const base::FilePath& output_file_path,
+ const DownloadActionCallback& download_action_callback,
+ const GetContentCallback& get_content_callback,
+ const ProgressCallback& progress_callback);
+ virtual ~DownloadFileRequest();
+
+ DISALLOW_COPY_AND_ASSIGN(DownloadFileRequest);
+};
+
+} // namespace drive
+} // namespace google_apis
+
+#endif // GOOGLE_APIS_DRIVE_DRIVE_API_REQUESTS_H_
diff --git a/chromium/google_apis/drive/drive_api_requests_unittest.cc b/chromium/google_apis/drive/drive_api_requests_unittest.cc
new file mode 100644
index 00000000000..466240cd369
--- /dev/null
+++ b/chromium/google_apis/drive/drive_api_requests_unittest.cc
@@ -0,0 +1,1647 @@
+// Copyright (c) 2013 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "base/bind.h"
+#include "base/file_util.h"
+#include "base/files/file_path.h"
+#include "base/files/scoped_temp_dir.h"
+#include "base/message_loop/message_loop.h"
+#include "base/run_loop.h"
+#include "base/strings/string_number_conversions.h"
+#include "base/values.h"
+#include "google_apis/drive/drive_api_parser.h"
+#include "google_apis/drive/drive_api_requests.h"
+#include "google_apis/drive/drive_api_url_generator.h"
+#include "google_apis/drive/dummy_auth_service.h"
+#include "google_apis/drive/request_sender.h"
+#include "google_apis/drive/test_util.h"
+#include "net/test/embedded_test_server/embedded_test_server.h"
+#include "net/test/embedded_test_server/http_request.h"
+#include "net/test/embedded_test_server/http_response.h"
+#include "net/url_request/url_request_test_util.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+namespace google_apis {
+
+namespace {
+
+const char kTestETag[] = "test_etag";
+const char kTestUserAgent[] = "test-user-agent";
+
+const char kTestChildrenResponse[] =
+ "{\n"
+ "\"kind\": \"drive#childReference\",\n"
+ "\"id\": \"resource_id\",\n"
+ "\"selfLink\": \"self_link\",\n"
+ "\"childLink\": \"child_link\",\n"
+ "}\n";
+
+const char kTestUploadExistingFilePath[] = "/upload/existingfile/path";
+const char kTestUploadNewFilePath[] = "/upload/newfile/path";
+const char kTestDownloadPathPrefix[] = "/download/";
+
+// Used as a GetContentCallback.
+void AppendContent(std::string* out,
+ GDataErrorCode error,
+ scoped_ptr<std::string> content) {
+ EXPECT_EQ(HTTP_SUCCESS, error);
+ out->append(*content);
+}
+
+} // namespace
+
+class DriveApiRequestsTest : public testing::Test {
+ public:
+ DriveApiRequestsTest() {
+ }
+
+ virtual void SetUp() OVERRIDE {
+ request_context_getter_ = new net::TestURLRequestContextGetter(
+ message_loop_.message_loop_proxy());
+
+ request_sender_.reset(new RequestSender(new DummyAuthService,
+ request_context_getter_.get(),
+ message_loop_.message_loop_proxy(),
+ kTestUserAgent));
+
+ ASSERT_TRUE(temp_dir_.CreateUniqueTempDir());
+
+ ASSERT_TRUE(test_server_.InitializeAndWaitUntilReady());
+ test_server_.RegisterRequestHandler(
+ base::Bind(&DriveApiRequestsTest::HandleChildrenDeleteRequest,
+ base::Unretained(this)));
+ test_server_.RegisterRequestHandler(
+ base::Bind(&DriveApiRequestsTest::HandleDataFileRequest,
+ base::Unretained(this)));
+ test_server_.RegisterRequestHandler(
+ base::Bind(&DriveApiRequestsTest::HandleDeleteRequest,
+ base::Unretained(this)));
+ test_server_.RegisterRequestHandler(
+ base::Bind(&DriveApiRequestsTest::HandlePreconditionFailedRequest,
+ base::Unretained(this)));
+ test_server_.RegisterRequestHandler(
+ base::Bind(&DriveApiRequestsTest::HandleResumeUploadRequest,
+ base::Unretained(this)));
+ test_server_.RegisterRequestHandler(
+ base::Bind(&DriveApiRequestsTest::HandleInitiateUploadRequest,
+ base::Unretained(this)));
+ test_server_.RegisterRequestHandler(
+ base::Bind(&DriveApiRequestsTest::HandleContentResponse,
+ base::Unretained(this)));
+ test_server_.RegisterRequestHandler(
+ base::Bind(&DriveApiRequestsTest::HandleDownloadRequest,
+ base::Unretained(this)));
+
+ GURL test_base_url = test_util::GetBaseUrlForTesting(test_server_.port());
+ url_generator_.reset(new DriveApiUrlGenerator(
+ test_base_url, test_base_url.Resolve(kTestDownloadPathPrefix)));
+
+ // Reset the server's expected behavior just in case.
+ ResetExpectedResponse();
+ received_bytes_ = 0;
+ content_length_ = 0;
+ }
+
+ base::MessageLoopForIO message_loop_; // Test server needs IO thread.
+ net::test_server::EmbeddedTestServer test_server_;
+ scoped_ptr<RequestSender> request_sender_;
+ scoped_ptr<DriveApiUrlGenerator> url_generator_;
+ scoped_refptr<net::TestURLRequestContextGetter> request_context_getter_;
+ base::ScopedTempDir temp_dir_;
+
+ // This is a path to the file which contains expected response from
+ // the server. See also HandleDataFileRequest below.
+ base::FilePath expected_data_file_path_;
+
+ // This is a path string in the expected response header from the server
+ // for initiating file uploading.
+ std::string expected_upload_path_;
+
+ // This is a path to the file which contains expected response for
+ // PRECONDITION_FAILED response.
+ base::FilePath expected_precondition_failed_file_path_;
+
+ // These are content and its type in the expected response from the server.
+ // See also HandleContentResponse below.
+ std::string expected_content_type_;
+ std::string expected_content_;
+
+ // The incoming HTTP request is saved so tests can verify the request
+ // parameters like HTTP method (ex. some requests should use DELETE
+ // instead of GET).
+ net::test_server::HttpRequest http_request_;
+
+ private:
+ void ResetExpectedResponse() {
+ expected_data_file_path_.clear();
+ expected_upload_path_.clear();
+ expected_content_type_.clear();
+ expected_content_.clear();
+ }
+
+ // For "Children: delete" request, the server will return "204 No Content"
+ // response meaning "success".
+ scoped_ptr<net::test_server::HttpResponse> HandleChildrenDeleteRequest(
+ const net::test_server::HttpRequest& request) {
+ if (request.method != net::test_server::METHOD_DELETE ||
+ request.relative_url.find("/children/") == string::npos) {
+ // The request is not the "Children: delete" request. Delegate the
+ // processing to the next handler.
+ return scoped_ptr<net::test_server::HttpResponse>();
+ }
+
+ http_request_ = request;
+
+ // Return the response with just "204 No Content" status code.
+ scoped_ptr<net::test_server::BasicHttpResponse> http_response(
+ new net::test_server::BasicHttpResponse);
+ http_response->set_code(net::HTTP_NO_CONTENT);
+ return http_response.PassAs<net::test_server::HttpResponse>();
+ }
+
+ // Reads the data file of |expected_data_file_path_| and returns its content
+ // for the request.
+ // To use this method, it is necessary to set |expected_data_file_path_|
+ // to the appropriate file path before sending the request to the server.
+ scoped_ptr<net::test_server::HttpResponse> HandleDataFileRequest(
+ const net::test_server::HttpRequest& request) {
+ if (expected_data_file_path_.empty()) {
+ // The file is not specified. Delegate the processing to the next
+ // handler.
+ return scoped_ptr<net::test_server::HttpResponse>();
+ }
+
+ http_request_ = request;
+
+ // Return the response from the data file.
+ return test_util::CreateHttpResponseFromFile(
+ expected_data_file_path_).PassAs<net::test_server::HttpResponse>();
+ }
+
+ // Deletes the resource and returns no content with HTTP_NO_CONTENT status
+ // code.
+ scoped_ptr<net::test_server::HttpResponse> HandleDeleteRequest(
+ const net::test_server::HttpRequest& request) {
+ if (request.method != net::test_server::METHOD_DELETE ||
+ request.relative_url.find("/files/") == string::npos) {
+ // The file is not file deletion request. Delegate the processing to the
+ // next handler.
+ return scoped_ptr<net::test_server::HttpResponse>();
+ }
+
+ http_request_ = request;
+
+ scoped_ptr<net::test_server::BasicHttpResponse> response(
+ new net::test_server::BasicHttpResponse);
+ response->set_code(net::HTTP_NO_CONTENT);
+
+ return response.PassAs<net::test_server::HttpResponse>();
+ }
+
+ // Returns PRECONDITION_FAILED response for ETag mismatching with error JSON
+ // content specified by |expected_precondition_failed_file_path_|.
+ // To use this method, it is necessary to set the variable to the appropriate
+ // file path before sending the request to the server.
+ scoped_ptr<net::test_server::HttpResponse> HandlePreconditionFailedRequest(
+ const net::test_server::HttpRequest& request) {
+ if (expected_precondition_failed_file_path_.empty()) {
+ // The file is not specified. Delegate the process to the next handler.
+ return scoped_ptr<net::test_server::HttpResponse>();
+ }
+
+ http_request_ = request;
+
+ scoped_ptr<net::test_server::BasicHttpResponse> response(
+ new net::test_server::BasicHttpResponse);
+ response->set_code(net::HTTP_PRECONDITION_FAILED);
+
+ std::string content;
+ if (base::ReadFileToString(expected_precondition_failed_file_path_,
+ &content)) {
+ response->set_content(content);
+ response->set_content_type("application/json");
+ }
+
+ return response.PassAs<net::test_server::HttpResponse>();
+ }
+
+ // Returns the response based on set expected upload url.
+ // The response contains the url in its "Location: " header. Also, it doesn't
+ // have any content.
+ // To use this method, it is necessary to set |expected_upload_path_|
+ // to the string representation of the url to be returned.
+ scoped_ptr<net::test_server::HttpResponse> HandleInitiateUploadRequest(
+ const net::test_server::HttpRequest& request) {
+ if (request.relative_url == expected_upload_path_ ||
+ expected_upload_path_.empty()) {
+ // The request is for resume uploading or the expected upload url is not
+ // set. Delegate the processing to the next handler.
+ return scoped_ptr<net::test_server::HttpResponse>();
+ }
+
+ http_request_ = request;
+
+ scoped_ptr<net::test_server::BasicHttpResponse> response(
+ new net::test_server::BasicHttpResponse);
+
+ // Check if the X-Upload-Content-Length is present. If yes, store the
+ // length of the file.
+ std::map<std::string, std::string>::const_iterator found =
+ request.headers.find("X-Upload-Content-Length");
+ if (found == request.headers.end() ||
+ !base::StringToInt64(found->second, &content_length_)) {
+ return scoped_ptr<net::test_server::HttpResponse>();
+ }
+ received_bytes_ = 0;
+
+ response->set_code(net::HTTP_OK);
+ response->AddCustomHeader(
+ "Location",
+ test_server_.base_url().Resolve(expected_upload_path_).spec());
+ return response.PassAs<net::test_server::HttpResponse>();
+ }
+
+ scoped_ptr<net::test_server::HttpResponse> HandleResumeUploadRequest(
+ const net::test_server::HttpRequest& request) {
+ if (request.relative_url != expected_upload_path_) {
+ // The request path is different from the expected path for uploading.
+ // Delegate the processing to the next handler.
+ return scoped_ptr<net::test_server::HttpResponse>();
+ }
+
+ http_request_ = request;
+
+ if (!request.content.empty()) {
+ std::map<std::string, std::string>::const_iterator iter =
+ request.headers.find("Content-Range");
+ if (iter == request.headers.end()) {
+ // The range must be set.
+ return scoped_ptr<net::test_server::HttpResponse>();
+ }
+
+ int64 length = 0;
+ int64 start_position = 0;
+ int64 end_position = 0;
+ if (!test_util::ParseContentRangeHeader(
+ iter->second, &start_position, &end_position, &length)) {
+ // Invalid "Content-Range" value.
+ return scoped_ptr<net::test_server::HttpResponse>();
+ }
+
+ EXPECT_EQ(start_position, received_bytes_);
+ EXPECT_EQ(length, content_length_);
+
+ // end_position is inclusive, but so +1 to change the range to byte size.
+ received_bytes_ = end_position + 1;
+ }
+
+ if (received_bytes_ < content_length_) {
+ scoped_ptr<net::test_server::BasicHttpResponse> response(
+ new net::test_server::BasicHttpResponse);
+ // Set RESUME INCOMPLETE (308) status code.
+ response->set_code(static_cast<net::HttpStatusCode>(308));
+
+ // Add Range header to the response, based on the values of
+ // Content-Range header in the request.
+ // The header is annotated only when at least one byte is received.
+ if (received_bytes_ > 0) {
+ response->AddCustomHeader(
+ "Range", "bytes=0-" + base::Int64ToString(received_bytes_ - 1));
+ }
+
+ return response.PassAs<net::test_server::HttpResponse>();
+ }
+
+ // All bytes are received. Return the "success" response with the file's
+ // (dummy) metadata.
+ scoped_ptr<net::test_server::BasicHttpResponse> response =
+ test_util::CreateHttpResponseFromFile(
+ test_util::GetTestFilePath("drive/file_entry.json"));
+
+ // The response code is CREATED if it is new file uploading.
+ if (http_request_.relative_url == kTestUploadNewFilePath) {
+ response->set_code(net::HTTP_CREATED);
+ }
+
+ return response.PassAs<net::test_server::HttpResponse>();
+ }
+
+ // Returns the response based on set expected content and its type.
+ // To use this method, both |expected_content_type_| and |expected_content_|
+ // must be set in advance.
+ scoped_ptr<net::test_server::HttpResponse> HandleContentResponse(
+ const net::test_server::HttpRequest& request) {
+ if (expected_content_type_.empty() || expected_content_.empty()) {
+ // Expected content is not set. Delegate the processing to the next
+ // handler.
+ return scoped_ptr<net::test_server::HttpResponse>();
+ }
+
+ http_request_ = request;
+
+ scoped_ptr<net::test_server::BasicHttpResponse> response(
+ new net::test_server::BasicHttpResponse);
+ response->set_code(net::HTTP_OK);
+ response->set_content_type(expected_content_type_);
+ response->set_content(expected_content_);
+ return response.PassAs<net::test_server::HttpResponse>();
+ }
+
+ // Handles a request for downloading a file.
+ scoped_ptr<net::test_server::HttpResponse> HandleDownloadRequest(
+ const net::test_server::HttpRequest& request) {
+ http_request_ = request;
+
+ const GURL absolute_url = test_server_.GetURL(request.relative_url);
+ std::string id;
+ if (!test_util::RemovePrefix(absolute_url.path(),
+ kTestDownloadPathPrefix,
+ &id)) {
+ return scoped_ptr<net::test_server::HttpResponse>();
+ }
+
+ // For testing, returns a text with |id| repeated 3 times.
+ scoped_ptr<net::test_server::BasicHttpResponse> response(
+ new net::test_server::BasicHttpResponse);
+ response->set_code(net::HTTP_OK);
+ response->set_content(id + id + id);
+ response->set_content_type("text/plain");
+ return response.PassAs<net::test_server::HttpResponse>();
+ }
+
+ // These are for the current upload file status.
+ int64 received_bytes_;
+ int64 content_length_;
+};
+
+TEST_F(DriveApiRequestsTest, DriveApiDataRequest_Fields) {
+ // Make sure that "fields" query param is supported by using its subclass,
+ // AboutGetRequest.
+
+ // Set an expected data file containing valid result.
+ expected_data_file_path_ = test_util::GetTestFilePath(
+ "drive/about.json");
+
+ GDataErrorCode error = GDATA_OTHER_ERROR;
+ scoped_ptr<AboutResource> about_resource;
+
+ {
+ base::RunLoop run_loop;
+ drive::AboutGetRequest* request = new drive::AboutGetRequest(
+ request_sender_.get(),
+ *url_generator_,
+ test_util::CreateQuitCallback(
+ &run_loop,
+ test_util::CreateCopyResultCallback(&error, &about_resource)));
+ request->set_fields(
+ "kind,quotaBytesTotal,quotaBytesUsed,largestChangeId,rootFolderId");
+ request_sender_->StartRequestWithRetry(request);
+ run_loop.Run();
+ }
+
+ EXPECT_EQ(HTTP_SUCCESS, error);
+ EXPECT_EQ(net::test_server::METHOD_GET, http_request_.method);
+ EXPECT_EQ("/drive/v2/about?"
+ "fields=kind%2CquotaBytesTotal%2CquotaBytesUsed%2C"
+ "largestChangeId%2CrootFolderId",
+ http_request_.relative_url);
+
+ scoped_ptr<AboutResource> expected(
+ AboutResource::CreateFrom(
+ *test_util::LoadJSONFile("drive/about.json")));
+ ASSERT_TRUE(about_resource.get());
+ EXPECT_EQ(expected->largest_change_id(), about_resource->largest_change_id());
+ EXPECT_EQ(expected->quota_bytes_total(), about_resource->quota_bytes_total());
+ EXPECT_EQ(expected->quota_bytes_used(), about_resource->quota_bytes_used());
+ EXPECT_EQ(expected->root_folder_id(), about_resource->root_folder_id());
+}
+
+TEST_F(DriveApiRequestsTest, FilesInsertRequest) {
+ // Set an expected data file containing the directory's entry data.
+ expected_data_file_path_ =
+ test_util::GetTestFilePath("drive/directory_entry.json");
+
+ GDataErrorCode error = GDATA_OTHER_ERROR;
+ scoped_ptr<FileResource> file_resource;
+
+ // Create "new directory" in the root directory.
+ {
+ base::RunLoop run_loop;
+ drive::FilesInsertRequest* request = new drive::FilesInsertRequest(
+ request_sender_.get(),
+ *url_generator_,
+ test_util::CreateQuitCallback(
+ &run_loop,
+ test_util::CreateCopyResultCallback(&error, &file_resource)));
+ request->set_mime_type("application/vnd.google-apps.folder");
+ request->add_parent("root");
+ request->set_title("new directory");
+ request_sender_->StartRequestWithRetry(request);
+ run_loop.Run();
+ }
+
+ EXPECT_EQ(HTTP_SUCCESS, error);
+ EXPECT_EQ(net::test_server::METHOD_POST, http_request_.method);
+ EXPECT_EQ("/drive/v2/files", http_request_.relative_url);
+ EXPECT_EQ("application/json", http_request_.headers["Content-Type"]);
+
+ EXPECT_TRUE(http_request_.has_content);
+
+ scoped_ptr<FileResource> expected(
+ FileResource::CreateFrom(
+ *test_util::LoadJSONFile("drive/directory_entry.json")));
+
+ // Sanity check.
+ ASSERT_TRUE(file_resource.get());
+
+ EXPECT_EQ(expected->file_id(), file_resource->file_id());
+ EXPECT_EQ(expected->title(), file_resource->title());
+ EXPECT_EQ(expected->mime_type(), file_resource->mime_type());
+ EXPECT_EQ(expected->parents().size(), file_resource->parents().size());
+}
+
+TEST_F(DriveApiRequestsTest, FilesPatchRequest) {
+ const base::Time::Exploded kModifiedDate = {2012, 7, 0, 19, 15, 59, 13, 123};
+ const base::Time::Exploded kLastViewedByMeDate =
+ {2013, 7, 0, 19, 15, 59, 13, 123};
+
+ // Set an expected data file containing valid result.
+ expected_data_file_path_ =
+ test_util::GetTestFilePath("drive/file_entry.json");
+
+ GDataErrorCode error = GDATA_OTHER_ERROR;
+ scoped_ptr<FileResource> file_resource;
+
+ {
+ base::RunLoop run_loop;
+ drive::FilesPatchRequest* request = new drive::FilesPatchRequest(
+ request_sender_.get(),
+ *url_generator_,
+ test_util::CreateQuitCallback(
+ &run_loop,
+ test_util::CreateCopyResultCallback(&error, &file_resource)));
+ request->set_file_id("resource_id");
+ request->set_set_modified_date(true);
+ request->set_update_viewed_date(false);
+
+ request->set_title("new title");
+ request->set_modified_date(base::Time::FromUTCExploded(kModifiedDate));
+ request->set_last_viewed_by_me_date(
+ base::Time::FromUTCExploded(kLastViewedByMeDate));
+ request->add_parent("parent_resource_id");
+
+ request_sender_->StartRequestWithRetry(request);
+ run_loop.Run();
+ }
+
+ EXPECT_EQ(HTTP_SUCCESS, error);
+ EXPECT_EQ(net::test_server::METHOD_PATCH, http_request_.method);
+ EXPECT_EQ("/drive/v2/files/resource_id"
+ "?setModifiedDate=true&updateViewedDate=false",
+ http_request_.relative_url);
+
+ EXPECT_EQ("application/json", http_request_.headers["Content-Type"]);
+ EXPECT_TRUE(http_request_.has_content);
+ EXPECT_EQ("{\"lastViewedByMeDate\":\"2013-07-19T15:59:13.123Z\","
+ "\"modifiedDate\":\"2012-07-19T15:59:13.123Z\","
+ "\"parents\":[{\"id\":\"parent_resource_id\"}],"
+ "\"title\":\"new title\"}",
+ http_request_.content);
+ EXPECT_TRUE(file_resource);
+}
+
+TEST_F(DriveApiRequestsTest, AboutGetRequest_ValidJson) {
+ // Set an expected data file containing valid result.
+ expected_data_file_path_ = test_util::GetTestFilePath(
+ "drive/about.json");
+
+ GDataErrorCode error = GDATA_OTHER_ERROR;
+ scoped_ptr<AboutResource> about_resource;
+
+ {
+ base::RunLoop run_loop;
+ drive::AboutGetRequest* request = new drive::AboutGetRequest(
+ request_sender_.get(),
+ *url_generator_,
+ test_util::CreateQuitCallback(
+ &run_loop,
+ test_util::CreateCopyResultCallback(&error, &about_resource)));
+ request_sender_->StartRequestWithRetry(request);
+ run_loop.Run();
+ }
+
+ EXPECT_EQ(HTTP_SUCCESS, error);
+ EXPECT_EQ(net::test_server::METHOD_GET, http_request_.method);
+ EXPECT_EQ("/drive/v2/about", http_request_.relative_url);
+
+ scoped_ptr<AboutResource> expected(
+ AboutResource::CreateFrom(
+ *test_util::LoadJSONFile("drive/about.json")));
+ ASSERT_TRUE(about_resource.get());
+ EXPECT_EQ(expected->largest_change_id(), about_resource->largest_change_id());
+ EXPECT_EQ(expected->quota_bytes_total(), about_resource->quota_bytes_total());
+ EXPECT_EQ(expected->quota_bytes_used(), about_resource->quota_bytes_used());
+ EXPECT_EQ(expected->root_folder_id(), about_resource->root_folder_id());
+}
+
+TEST_F(DriveApiRequestsTest, AboutGetRequest_InvalidJson) {
+ // Set an expected data file containing invalid result.
+ expected_data_file_path_ = test_util::GetTestFilePath(
+ "gdata/testfile.txt");
+
+ GDataErrorCode error = GDATA_OTHER_ERROR;
+ scoped_ptr<AboutResource> about_resource;
+
+ {
+ base::RunLoop run_loop;
+ drive::AboutGetRequest* request = new drive::AboutGetRequest(
+ request_sender_.get(),
+ *url_generator_,
+ test_util::CreateQuitCallback(
+ &run_loop,
+ test_util::CreateCopyResultCallback(&error, &about_resource)));
+ request_sender_->StartRequestWithRetry(request);
+ run_loop.Run();
+ }
+
+ // "parse error" should be returned, and the about resource should be NULL.
+ EXPECT_EQ(GDATA_PARSE_ERROR, error);
+ EXPECT_EQ(net::test_server::METHOD_GET, http_request_.method);
+ EXPECT_EQ("/drive/v2/about", http_request_.relative_url);
+ EXPECT_FALSE(about_resource);
+}
+
+TEST_F(DriveApiRequestsTest, AppsListRequest) {
+ // Set an expected data file containing valid result.
+ expected_data_file_path_ = test_util::GetTestFilePath(
+ "drive/applist.json");
+
+ GDataErrorCode error = GDATA_OTHER_ERROR;
+ scoped_ptr<AppList> app_list;
+
+ {
+ base::RunLoop run_loop;
+ drive::AppsListRequest* request = new drive::AppsListRequest(
+ request_sender_.get(),
+ *url_generator_,
+ test_util::CreateQuitCallback(
+ &run_loop,
+ test_util::CreateCopyResultCallback(&error, &app_list)));
+ request_sender_->StartRequestWithRetry(request);
+ run_loop.Run();
+ }
+
+ EXPECT_EQ(HTTP_SUCCESS, error);
+ EXPECT_EQ(net::test_server::METHOD_GET, http_request_.method);
+ EXPECT_EQ("/drive/v2/apps", http_request_.relative_url);
+ EXPECT_TRUE(app_list);
+}
+
+TEST_F(DriveApiRequestsTest, ChangesListRequest) {
+ // Set an expected data file containing valid result.
+ expected_data_file_path_ = test_util::GetTestFilePath(
+ "drive/changelist.json");
+
+ GDataErrorCode error = GDATA_OTHER_ERROR;
+ scoped_ptr<ChangeList> result;
+
+ {
+ base::RunLoop run_loop;
+ drive::ChangesListRequest* request = new drive::ChangesListRequest(
+ request_sender_.get(), *url_generator_,
+ test_util::CreateQuitCallback(
+ &run_loop,
+ test_util::CreateCopyResultCallback(&error, &result)));
+ request->set_include_deleted(true);
+ request->set_start_change_id(100);
+ request->set_max_results(500);
+ request_sender_->StartRequestWithRetry(request);
+ run_loop.Run();
+ }
+
+ EXPECT_EQ(HTTP_SUCCESS, error);
+ EXPECT_EQ(net::test_server::METHOD_GET, http_request_.method);
+ EXPECT_EQ("/drive/v2/changes?maxResults=500&startChangeId=100",
+ http_request_.relative_url);
+ EXPECT_TRUE(result);
+}
+
+TEST_F(DriveApiRequestsTest, ChangesListNextPageRequest) {
+ // Set an expected data file containing valid result.
+ expected_data_file_path_ = test_util::GetTestFilePath(
+ "drive/changelist.json");
+
+ GDataErrorCode error = GDATA_OTHER_ERROR;
+ scoped_ptr<ChangeList> result;
+
+ {
+ base::RunLoop run_loop;
+ drive::ChangesListNextPageRequest* request =
+ new drive::ChangesListNextPageRequest(
+ request_sender_.get(),
+ test_util::CreateQuitCallback(
+ &run_loop,
+ test_util::CreateCopyResultCallback(&error, &result)));
+ request->set_next_link(test_server_.GetURL("/continue/get/change/list"));
+ request_sender_->StartRequestWithRetry(request);
+ run_loop.Run();
+ }
+
+ EXPECT_EQ(HTTP_SUCCESS, error);
+ EXPECT_EQ(net::test_server::METHOD_GET, http_request_.method);
+ EXPECT_EQ("/continue/get/change/list", http_request_.relative_url);
+ EXPECT_TRUE(result);
+}
+
+TEST_F(DriveApiRequestsTest, FilesCopyRequest) {
+ const base::Time::Exploded kModifiedDate = {2012, 7, 0, 19, 15, 59, 13, 123};
+
+ // Set an expected data file containing the dummy file entry data.
+ // It'd be returned if we copy a file.
+ expected_data_file_path_ =
+ test_util::GetTestFilePath("drive/file_entry.json");
+
+ GDataErrorCode error = GDATA_OTHER_ERROR;
+ scoped_ptr<FileResource> file_resource;
+
+ // Copy the file to a new file named "new title".
+ {
+ base::RunLoop run_loop;
+ drive::FilesCopyRequest* request = new drive::FilesCopyRequest(
+ request_sender_.get(),
+ *url_generator_,
+ test_util::CreateQuitCallback(
+ &run_loop,
+ test_util::CreateCopyResultCallback(&error, &file_resource)));
+ request->set_file_id("resource_id");
+ request->set_modified_date(base::Time::FromUTCExploded(kModifiedDate));
+ request->add_parent("parent_resource_id");
+ request->set_title("new title");
+ request_sender_->StartRequestWithRetry(request);
+ run_loop.Run();
+ }
+
+ EXPECT_EQ(HTTP_SUCCESS, error);
+ EXPECT_EQ(net::test_server::METHOD_POST, http_request_.method);
+ EXPECT_EQ("/drive/v2/files/resource_id/copy", http_request_.relative_url);
+ EXPECT_EQ("application/json", http_request_.headers["Content-Type"]);
+
+ EXPECT_TRUE(http_request_.has_content);
+ EXPECT_EQ(
+ "{\"modifiedDate\":\"2012-07-19T15:59:13.123Z\","
+ "\"parents\":[{\"id\":\"parent_resource_id\"}],\"title\":\"new title\"}",
+ http_request_.content);
+ EXPECT_TRUE(file_resource);
+}
+
+TEST_F(DriveApiRequestsTest, FilesCopyRequest_EmptyParentResourceId) {
+ // Set an expected data file containing the dummy file entry data.
+ // It'd be returned if we copy a file.
+ expected_data_file_path_ =
+ test_util::GetTestFilePath("drive/file_entry.json");
+
+ GDataErrorCode error = GDATA_OTHER_ERROR;
+ scoped_ptr<FileResource> file_resource;
+
+ // Copy the file to a new file named "new title".
+ {
+ base::RunLoop run_loop;
+ drive::FilesCopyRequest* request = new drive::FilesCopyRequest(
+ request_sender_.get(),
+ *url_generator_,
+ test_util::CreateQuitCallback(
+ &run_loop,
+ test_util::CreateCopyResultCallback(&error, &file_resource)));
+ request->set_file_id("resource_id");
+ request->set_title("new title");
+ request_sender_->StartRequestWithRetry(request);
+ run_loop.Run();
+ }
+
+ EXPECT_EQ(HTTP_SUCCESS, error);
+ EXPECT_EQ(net::test_server::METHOD_POST, http_request_.method);
+ EXPECT_EQ("/drive/v2/files/resource_id/copy", http_request_.relative_url);
+ EXPECT_EQ("application/json", http_request_.headers["Content-Type"]);
+
+ EXPECT_TRUE(http_request_.has_content);
+ EXPECT_EQ("{\"title\":\"new title\"}", http_request_.content);
+ EXPECT_TRUE(file_resource);
+}
+
+TEST_F(DriveApiRequestsTest, FilesListRequest) {
+ // Set an expected data file containing valid result.
+ expected_data_file_path_ = test_util::GetTestFilePath(
+ "drive/filelist.json");
+
+ GDataErrorCode error = GDATA_OTHER_ERROR;
+ scoped_ptr<FileList> result;
+
+ {
+ base::RunLoop run_loop;
+ drive::FilesListRequest* request = new drive::FilesListRequest(
+ request_sender_.get(), *url_generator_,
+ test_util::CreateQuitCallback(
+ &run_loop,
+ test_util::CreateCopyResultCallback(&error, &result)));
+ request->set_max_results(50);
+ request->set_q("\"abcde\" in parents");
+ request_sender_->StartRequestWithRetry(request);
+ run_loop.Run();
+ }
+
+ EXPECT_EQ(HTTP_SUCCESS, error);
+ EXPECT_EQ(net::test_server::METHOD_GET, http_request_.method);
+ EXPECT_EQ("/drive/v2/files?maxResults=50&q=%22abcde%22+in+parents",
+ http_request_.relative_url);
+ EXPECT_TRUE(result);
+}
+
+TEST_F(DriveApiRequestsTest, FilesListNextPageRequest) {
+ // Set an expected data file containing valid result.
+ expected_data_file_path_ = test_util::GetTestFilePath(
+ "drive/filelist.json");
+
+ GDataErrorCode error = GDATA_OTHER_ERROR;
+ scoped_ptr<FileList> result;
+
+ {
+ base::RunLoop run_loop;
+ drive::FilesListNextPageRequest* request =
+ new drive::FilesListNextPageRequest(
+ request_sender_.get(),
+ test_util::CreateQuitCallback(
+ &run_loop,
+ test_util::CreateCopyResultCallback(&error, &result)));
+ request->set_next_link(test_server_.GetURL("/continue/get/file/list"));
+ request_sender_->StartRequestWithRetry(request);
+ run_loop.Run();
+ }
+
+ EXPECT_EQ(HTTP_SUCCESS, error);
+ EXPECT_EQ(net::test_server::METHOD_GET, http_request_.method);
+ EXPECT_EQ("/continue/get/file/list", http_request_.relative_url);
+ EXPECT_TRUE(result);
+}
+
+TEST_F(DriveApiRequestsTest, FilesDeleteRequest) {
+ GDataErrorCode error = GDATA_OTHER_ERROR;
+
+ // Delete a resource with the given resource id.
+ {
+ base::RunLoop run_loop;
+ drive::FilesDeleteRequest* request = new drive::FilesDeleteRequest(
+ request_sender_.get(),
+ *url_generator_,
+ test_util::CreateQuitCallback(
+ &run_loop, test_util::CreateCopyResultCallback(&error)));
+ request->set_file_id("resource_id");
+ request->set_etag(kTestETag);
+ request_sender_->StartRequestWithRetry(request);
+ run_loop.Run();
+ }
+
+ EXPECT_EQ(HTTP_NO_CONTENT, error);
+ EXPECT_EQ(net::test_server::METHOD_DELETE, http_request_.method);
+ EXPECT_EQ(kTestETag, http_request_.headers["If-Match"]);
+ EXPECT_EQ("/drive/v2/files/resource_id", http_request_.relative_url);
+ EXPECT_FALSE(http_request_.has_content);
+}
+
+TEST_F(DriveApiRequestsTest, FilesTrashRequest) {
+ // Set data for the expected result. Directory entry should be returned
+ // if the trashing entry is a directory, so using it here should be fine.
+ expected_data_file_path_ =
+ test_util::GetTestFilePath("drive/directory_entry.json");
+
+ GDataErrorCode error = GDATA_OTHER_ERROR;
+ scoped_ptr<FileResource> file_resource;
+
+ // Trash a resource with the given resource id.
+ {
+ base::RunLoop run_loop;
+ drive::FilesTrashRequest* request = new drive::FilesTrashRequest(
+ request_sender_.get(),
+ *url_generator_,
+ test_util::CreateQuitCallback(
+ &run_loop,
+ test_util::CreateCopyResultCallback(&error, &file_resource)));
+ request->set_file_id("resource_id");
+ request_sender_->StartRequestWithRetry(request);
+ run_loop.Run();
+ }
+
+ EXPECT_EQ(HTTP_SUCCESS, error);
+ EXPECT_EQ(net::test_server::METHOD_POST, http_request_.method);
+ EXPECT_EQ("/drive/v2/files/resource_id/trash", http_request_.relative_url);
+ EXPECT_TRUE(http_request_.has_content);
+ EXPECT_TRUE(http_request_.content.empty());
+}
+
+TEST_F(DriveApiRequestsTest, ChildrenInsertRequest) {
+ // Set an expected data file containing the children entry.
+ expected_content_type_ = "application/json";
+ expected_content_ = kTestChildrenResponse;
+
+ GDataErrorCode error = GDATA_OTHER_ERROR;
+
+ // Add a resource with "resource_id" to a directory with
+ // "parent_resource_id".
+ {
+ base::RunLoop run_loop;
+ drive::ChildrenInsertRequest* request = new drive::ChildrenInsertRequest(
+ request_sender_.get(),
+ *url_generator_,
+ test_util::CreateQuitCallback(
+ &run_loop,
+ test_util::CreateCopyResultCallback(&error)));
+ request->set_folder_id("parent_resource_id");
+ request->set_id("resource_id");
+ request_sender_->StartRequestWithRetry(request);
+ run_loop.Run();
+ }
+
+ EXPECT_EQ(HTTP_SUCCESS, error);
+ EXPECT_EQ(net::test_server::METHOD_POST, http_request_.method);
+ EXPECT_EQ("/drive/v2/files/parent_resource_id/children",
+ http_request_.relative_url);
+ EXPECT_EQ("application/json", http_request_.headers["Content-Type"]);
+
+ EXPECT_TRUE(http_request_.has_content);
+ EXPECT_EQ("{\"id\":\"resource_id\"}", http_request_.content);
+}
+
+TEST_F(DriveApiRequestsTest, ChildrenDeleteRequest) {
+ GDataErrorCode error = GDATA_OTHER_ERROR;
+
+ // Remove a resource with "resource_id" from a directory with
+ // "parent_resource_id".
+ {
+ base::RunLoop run_loop;
+ drive::ChildrenDeleteRequest* request = new drive::ChildrenDeleteRequest(
+ request_sender_.get(),
+ *url_generator_,
+ test_util::CreateQuitCallback(
+ &run_loop,
+ test_util::CreateCopyResultCallback(&error)));
+ request->set_child_id("resource_id");
+ request->set_folder_id("parent_resource_id");
+ request_sender_->StartRequestWithRetry(request);
+ run_loop.Run();
+ }
+
+ EXPECT_EQ(HTTP_NO_CONTENT, error);
+ EXPECT_EQ(net::test_server::METHOD_DELETE, http_request_.method);
+ EXPECT_EQ("/drive/v2/files/parent_resource_id/children/resource_id",
+ http_request_.relative_url);
+ EXPECT_FALSE(http_request_.has_content);
+}
+
+TEST_F(DriveApiRequestsTest, UploadNewFileRequest) {
+ // Set an expected url for uploading.
+ expected_upload_path_ = kTestUploadNewFilePath;
+
+ const char kTestContentType[] = "text/plain";
+ const std::string kTestContent(100, 'a');
+ const base::FilePath kTestFilePath =
+ temp_dir_.path().AppendASCII("upload_file.txt");
+ ASSERT_TRUE(test_util::WriteStringToFile(kTestFilePath, kTestContent));
+
+ GDataErrorCode error = GDATA_OTHER_ERROR;
+ GURL upload_url;
+
+ // Initiate uploading a new file to the directory with
+ // "parent_resource_id".
+ {
+ base::RunLoop run_loop;
+ drive::InitiateUploadNewFileRequest* request =
+ new drive::InitiateUploadNewFileRequest(
+ request_sender_.get(),
+ *url_generator_,
+ kTestContentType,
+ kTestContent.size(),
+ "parent_resource_id", // The resource id of the parent directory.
+ "new file title", // The title of the file being uploaded.
+ test_util::CreateQuitCallback(
+ &run_loop,
+ test_util::CreateCopyResultCallback(&error, &upload_url)));
+ request_sender_->StartRequestWithRetry(request);
+ run_loop.Run();
+ }
+
+ EXPECT_EQ(HTTP_SUCCESS, error);
+ EXPECT_EQ(kTestUploadNewFilePath, upload_url.path());
+ EXPECT_EQ(kTestContentType, http_request_.headers["X-Upload-Content-Type"]);
+ EXPECT_EQ(base::Int64ToString(kTestContent.size()),
+ http_request_.headers["X-Upload-Content-Length"]);
+
+ EXPECT_EQ(net::test_server::METHOD_POST, http_request_.method);
+ EXPECT_EQ("/upload/drive/v2/files?uploadType=resumable",
+ http_request_.relative_url);
+ EXPECT_EQ("application/json", http_request_.headers["Content-Type"]);
+ EXPECT_TRUE(http_request_.has_content);
+ EXPECT_EQ("{\"parents\":[{"
+ "\"id\":\"parent_resource_id\","
+ "\"kind\":\"drive#fileLink\""
+ "}],"
+ "\"title\":\"new file title\"}",
+ http_request_.content);
+
+ // Upload the content to the upload URL.
+ UploadRangeResponse response;
+ scoped_ptr<FileResource> new_entry;
+
+ {
+ base::RunLoop run_loop;
+ drive::ResumeUploadRequest* resume_request =
+ new drive::ResumeUploadRequest(
+ request_sender_.get(),
+ upload_url,
+ 0, // start_position
+ kTestContent.size(), // end_position (exclusive)
+ kTestContent.size(), // content_length,
+ kTestContentType,
+ kTestFilePath,
+ test_util::CreateQuitCallback(
+ &run_loop,
+ test_util::CreateCopyResultCallback(&response, &new_entry)),
+ ProgressCallback());
+ request_sender_->StartRequestWithRetry(resume_request);
+ run_loop.Run();
+ }
+
+ // METHOD_PUT should be used to upload data.
+ EXPECT_EQ(net::test_server::METHOD_PUT, http_request_.method);
+ // Request should go to the upload URL.
+ EXPECT_EQ(upload_url.path(), http_request_.relative_url);
+ // Content-Range header should be added.
+ EXPECT_EQ("bytes 0-" +
+ base::Int64ToString(kTestContent.size() - 1) + "/" +
+ base::Int64ToString(kTestContent.size()),
+ http_request_.headers["Content-Range"]);
+ // The upload content should be set in the HTTP request.
+ EXPECT_TRUE(http_request_.has_content);
+ EXPECT_EQ(kTestContent, http_request_.content);
+
+ // Check the response.
+ EXPECT_EQ(HTTP_CREATED, response.code); // Because it's a new file
+ // The start and end positions should be set to -1, if an upload is complete.
+ EXPECT_EQ(-1, response.start_position_received);
+ EXPECT_EQ(-1, response.end_position_received);
+}
+
+TEST_F(DriveApiRequestsTest, UploadNewEmptyFileRequest) {
+ // Set an expected url for uploading.
+ expected_upload_path_ = kTestUploadNewFilePath;
+
+ const char kTestContentType[] = "text/plain";
+ const char kTestContent[] = "";
+ const base::FilePath kTestFilePath =
+ temp_dir_.path().AppendASCII("empty_file.txt");
+ ASSERT_TRUE(test_util::WriteStringToFile(kTestFilePath, kTestContent));
+
+ GDataErrorCode error = GDATA_OTHER_ERROR;
+ GURL upload_url;
+
+ // Initiate uploading a new file to the directory with "parent_resource_id".
+ {
+ base::RunLoop run_loop;
+ drive::InitiateUploadNewFileRequest* request =
+ new drive::InitiateUploadNewFileRequest(
+ request_sender_.get(),
+ *url_generator_,
+ kTestContentType,
+ 0,
+ "parent_resource_id", // The resource id of the parent directory.
+ "new file title", // The title of the file being uploaded.
+ test_util::CreateQuitCallback(
+ &run_loop,
+ test_util::CreateCopyResultCallback(&error, &upload_url)));
+ request_sender_->StartRequestWithRetry(request);
+ run_loop.Run();
+ }
+
+ EXPECT_EQ(HTTP_SUCCESS, error);
+ EXPECT_EQ(kTestUploadNewFilePath, upload_url.path());
+ EXPECT_EQ(kTestContentType, http_request_.headers["X-Upload-Content-Type"]);
+ EXPECT_EQ("0", http_request_.headers["X-Upload-Content-Length"]);
+
+ EXPECT_EQ(net::test_server::METHOD_POST, http_request_.method);
+ EXPECT_EQ("/upload/drive/v2/files?uploadType=resumable",
+ http_request_.relative_url);
+ EXPECT_EQ("application/json", http_request_.headers["Content-Type"]);
+ EXPECT_TRUE(http_request_.has_content);
+ EXPECT_EQ("{\"parents\":[{"
+ "\"id\":\"parent_resource_id\","
+ "\"kind\":\"drive#fileLink\""
+ "}],"
+ "\"title\":\"new file title\"}",
+ http_request_.content);
+
+ // Upload the content to the upload URL.
+ UploadRangeResponse response;
+ scoped_ptr<FileResource> new_entry;
+
+ {
+ base::RunLoop run_loop;
+ drive::ResumeUploadRequest* resume_request =
+ new drive::ResumeUploadRequest(
+ request_sender_.get(),
+ upload_url,
+ 0, // start_position
+ 0, // end_position (exclusive)
+ 0, // content_length,
+ kTestContentType,
+ kTestFilePath,
+ test_util::CreateQuitCallback(
+ &run_loop,
+ test_util::CreateCopyResultCallback(&response, &new_entry)),
+ ProgressCallback());
+ request_sender_->StartRequestWithRetry(resume_request);
+ run_loop.Run();
+ }
+
+ // METHOD_PUT should be used to upload data.
+ EXPECT_EQ(net::test_server::METHOD_PUT, http_request_.method);
+ // Request should go to the upload URL.
+ EXPECT_EQ(upload_url.path(), http_request_.relative_url);
+ // Content-Range header should NOT be added.
+ EXPECT_EQ(0U, http_request_.headers.count("Content-Range"));
+ // The upload content should be set in the HTTP request.
+ EXPECT_TRUE(http_request_.has_content);
+ EXPECT_EQ(kTestContent, http_request_.content);
+
+ // Check the response.
+ EXPECT_EQ(HTTP_CREATED, response.code); // Because it's a new file
+ // The start and end positions should be set to -1, if an upload is complete.
+ EXPECT_EQ(-1, response.start_position_received);
+ EXPECT_EQ(-1, response.end_position_received);
+}
+
+TEST_F(DriveApiRequestsTest, UploadNewLargeFileRequest) {
+ // Set an expected url for uploading.
+ expected_upload_path_ = kTestUploadNewFilePath;
+
+ const char kTestContentType[] = "text/plain";
+ const size_t kNumChunkBytes = 10; // Num bytes in a chunk.
+ const std::string kTestContent(100, 'a');
+ const base::FilePath kTestFilePath =
+ temp_dir_.path().AppendASCII("upload_file.txt");
+ ASSERT_TRUE(test_util::WriteStringToFile(kTestFilePath, kTestContent));
+
+ GDataErrorCode error = GDATA_OTHER_ERROR;
+ GURL upload_url;
+
+ // Initiate uploading a new file to the directory with "parent_resource_id".
+ {
+ base::RunLoop run_loop;
+ drive::InitiateUploadNewFileRequest* request =
+ new drive::InitiateUploadNewFileRequest(
+ request_sender_.get(),
+ *url_generator_,
+ kTestContentType,
+ kTestContent.size(),
+ "parent_resource_id", // The resource id of the parent directory.
+ "new file title", // The title of the file being uploaded.
+ test_util::CreateQuitCallback(
+ &run_loop,
+ test_util::CreateCopyResultCallback(&error, &upload_url)));
+ request_sender_->StartRequestWithRetry(request);
+ run_loop.Run();
+ }
+
+ EXPECT_EQ(HTTP_SUCCESS, error);
+ EXPECT_EQ(kTestUploadNewFilePath, upload_url.path());
+ EXPECT_EQ(kTestContentType, http_request_.headers["X-Upload-Content-Type"]);
+ EXPECT_EQ(base::Int64ToString(kTestContent.size()),
+ http_request_.headers["X-Upload-Content-Length"]);
+
+ EXPECT_EQ(net::test_server::METHOD_POST, http_request_.method);
+ EXPECT_EQ("/upload/drive/v2/files?uploadType=resumable",
+ http_request_.relative_url);
+ EXPECT_EQ("application/json", http_request_.headers["Content-Type"]);
+ EXPECT_TRUE(http_request_.has_content);
+ EXPECT_EQ("{\"parents\":[{"
+ "\"id\":\"parent_resource_id\","
+ "\"kind\":\"drive#fileLink\""
+ "}],"
+ "\"title\":\"new file title\"}",
+ http_request_.content);
+
+ // Before sending any data, check the current status.
+ // This is an edge case test for GetUploadStatusRequest.
+ {
+ UploadRangeResponse response;
+ scoped_ptr<FileResource> new_entry;
+
+ // Check the response by GetUploadStatusRequest.
+ {
+ base::RunLoop run_loop;
+ drive::GetUploadStatusRequest* get_upload_status_request =
+ new drive::GetUploadStatusRequest(
+ request_sender_.get(),
+ upload_url,
+ kTestContent.size(),
+ test_util::CreateQuitCallback(
+ &run_loop,
+ test_util::CreateCopyResultCallback(&response, &new_entry)));
+ request_sender_->StartRequestWithRetry(get_upload_status_request);
+ run_loop.Run();
+ }
+
+ // METHOD_PUT should be used to upload data.
+ EXPECT_EQ(net::test_server::METHOD_PUT, http_request_.method);
+ // Request should go to the upload URL.
+ EXPECT_EQ(upload_url.path(), http_request_.relative_url);
+ // Content-Range header should be added.
+ EXPECT_EQ("bytes */" + base::Int64ToString(kTestContent.size()),
+ http_request_.headers["Content-Range"]);
+ EXPECT_TRUE(http_request_.has_content);
+ EXPECT_TRUE(http_request_.content.empty());
+
+ // Check the response.
+ EXPECT_EQ(HTTP_RESUME_INCOMPLETE, response.code);
+ EXPECT_EQ(0, response.start_position_received);
+ EXPECT_EQ(0, response.end_position_received);
+ }
+
+ // Upload the content to the upload URL.
+ for (size_t start_position = 0; start_position < kTestContent.size();
+ start_position += kNumChunkBytes) {
+ const std::string payload = kTestContent.substr(
+ start_position,
+ std::min(kNumChunkBytes, kTestContent.size() - start_position));
+ const size_t end_position = start_position + payload.size();
+
+ UploadRangeResponse response;
+ scoped_ptr<FileResource> new_entry;
+
+ {
+ base::RunLoop run_loop;
+ drive::ResumeUploadRequest* resume_request =
+ new drive::ResumeUploadRequest(
+ request_sender_.get(),
+ upload_url,
+ start_position,
+ end_position,
+ kTestContent.size(), // content_length,
+ kTestContentType,
+ kTestFilePath,
+ test_util::CreateQuitCallback(
+ &run_loop,
+ test_util::CreateCopyResultCallback(&response, &new_entry)),
+ ProgressCallback());
+ request_sender_->StartRequestWithRetry(resume_request);
+ run_loop.Run();
+ }
+
+ // METHOD_PUT should be used to upload data.
+ EXPECT_EQ(net::test_server::METHOD_PUT, http_request_.method);
+ // Request should go to the upload URL.
+ EXPECT_EQ(upload_url.path(), http_request_.relative_url);
+ // Content-Range header should be added.
+ EXPECT_EQ("bytes " +
+ base::Int64ToString(start_position) + "-" +
+ base::Int64ToString(end_position - 1) + "/" +
+ base::Int64ToString(kTestContent.size()),
+ http_request_.headers["Content-Range"]);
+ // The upload content should be set in the HTTP request.
+ EXPECT_TRUE(http_request_.has_content);
+ EXPECT_EQ(payload, http_request_.content);
+
+ if (end_position == kTestContent.size()) {
+ // Check the response.
+ EXPECT_EQ(HTTP_CREATED, response.code); // Because it's a new file
+ // The start and end positions should be set to -1, if an upload is
+ // complete.
+ EXPECT_EQ(-1, response.start_position_received);
+ EXPECT_EQ(-1, response.end_position_received);
+ break;
+ }
+
+ // Check the response.
+ EXPECT_EQ(HTTP_RESUME_INCOMPLETE, response.code);
+ EXPECT_EQ(0, response.start_position_received);
+ EXPECT_EQ(static_cast<int64>(end_position), response.end_position_received);
+
+ // Check the response by GetUploadStatusRequest.
+ {
+ base::RunLoop run_loop;
+ drive::GetUploadStatusRequest* get_upload_status_request =
+ new drive::GetUploadStatusRequest(
+ request_sender_.get(),
+ upload_url,
+ kTestContent.size(),
+ test_util::CreateQuitCallback(
+ &run_loop,
+ test_util::CreateCopyResultCallback(&response, &new_entry)));
+ request_sender_->StartRequestWithRetry(get_upload_status_request);
+ run_loop.Run();
+ }
+
+ // METHOD_PUT should be used to upload data.
+ EXPECT_EQ(net::test_server::METHOD_PUT, http_request_.method);
+ // Request should go to the upload URL.
+ EXPECT_EQ(upload_url.path(), http_request_.relative_url);
+ // Content-Range header should be added.
+ EXPECT_EQ("bytes */" + base::Int64ToString(kTestContent.size()),
+ http_request_.headers["Content-Range"]);
+ EXPECT_TRUE(http_request_.has_content);
+ EXPECT_TRUE(http_request_.content.empty());
+
+ // Check the response.
+ EXPECT_EQ(HTTP_RESUME_INCOMPLETE, response.code);
+ EXPECT_EQ(0, response.start_position_received);
+ EXPECT_EQ(static_cast<int64>(end_position),
+ response.end_position_received);
+ }
+}
+
+TEST_F(DriveApiRequestsTest, UploadExistingFileRequest) {
+ // Set an expected url for uploading.
+ expected_upload_path_ = kTestUploadExistingFilePath;
+
+ const char kTestContentType[] = "text/plain";
+ const std::string kTestContent(100, 'a');
+ const base::FilePath kTestFilePath =
+ temp_dir_.path().AppendASCII("upload_file.txt");
+ ASSERT_TRUE(test_util::WriteStringToFile(kTestFilePath, kTestContent));
+
+ GDataErrorCode error = GDATA_OTHER_ERROR;
+ GURL upload_url;
+
+ // Initiate uploading a new file to the directory with "parent_resource_id".
+ {
+ base::RunLoop run_loop;
+ drive::InitiateUploadExistingFileRequest* request =
+ new drive::InitiateUploadExistingFileRequest(
+ request_sender_.get(),
+ *url_generator_,
+ kTestContentType,
+ kTestContent.size(),
+ "resource_id", // The resource id of the file to be overwritten.
+ std::string(), // No etag.
+ test_util::CreateQuitCallback(
+ &run_loop,
+ test_util::CreateCopyResultCallback(&error, &upload_url)));
+ request_sender_->StartRequestWithRetry(request);
+ run_loop.Run();
+ }
+
+ EXPECT_EQ(HTTP_SUCCESS, error);
+ EXPECT_EQ(kTestUploadExistingFilePath, upload_url.path());
+ EXPECT_EQ(kTestContentType, http_request_.headers["X-Upload-Content-Type"]);
+ EXPECT_EQ(base::Int64ToString(kTestContent.size()),
+ http_request_.headers["X-Upload-Content-Length"]);
+ EXPECT_EQ("*", http_request_.headers["If-Match"]);
+
+ EXPECT_EQ(net::test_server::METHOD_PUT, http_request_.method);
+ EXPECT_EQ("/upload/drive/v2/files/resource_id?uploadType=resumable",
+ http_request_.relative_url);
+ EXPECT_TRUE(http_request_.has_content);
+ EXPECT_TRUE(http_request_.content.empty());
+
+ // Upload the content to the upload URL.
+ UploadRangeResponse response;
+ scoped_ptr<FileResource> new_entry;
+
+ {
+ base::RunLoop run_loop;
+ drive::ResumeUploadRequest* resume_request =
+ new drive::ResumeUploadRequest(
+ request_sender_.get(),
+ upload_url,
+ 0, // start_position
+ kTestContent.size(), // end_position (exclusive)
+ kTestContent.size(), // content_length,
+ kTestContentType,
+ kTestFilePath,
+ test_util::CreateQuitCallback(
+ &run_loop,
+ test_util::CreateCopyResultCallback(&response, &new_entry)),
+ ProgressCallback());
+ request_sender_->StartRequestWithRetry(resume_request);
+ run_loop.Run();
+ }
+
+ // METHOD_PUT should be used to upload data.
+ EXPECT_EQ(net::test_server::METHOD_PUT, http_request_.method);
+ // Request should go to the upload URL.
+ EXPECT_EQ(upload_url.path(), http_request_.relative_url);
+ // Content-Range header should be added.
+ EXPECT_EQ("bytes 0-" +
+ base::Int64ToString(kTestContent.size() - 1) + "/" +
+ base::Int64ToString(kTestContent.size()),
+ http_request_.headers["Content-Range"]);
+ // The upload content should be set in the HTTP request.
+ EXPECT_TRUE(http_request_.has_content);
+ EXPECT_EQ(kTestContent, http_request_.content);
+
+ // Check the response.
+ EXPECT_EQ(HTTP_SUCCESS, response.code); // Because it's an existing file
+ // The start and end positions should be set to -1, if an upload is complete.
+ EXPECT_EQ(-1, response.start_position_received);
+ EXPECT_EQ(-1, response.end_position_received);
+}
+
+TEST_F(DriveApiRequestsTest, UploadExistingFileRequestWithETag) {
+ // Set an expected url for uploading.
+ expected_upload_path_ = kTestUploadExistingFilePath;
+
+ const char kTestContentType[] = "text/plain";
+ const std::string kTestContent(100, 'a');
+ const base::FilePath kTestFilePath =
+ temp_dir_.path().AppendASCII("upload_file.txt");
+ ASSERT_TRUE(test_util::WriteStringToFile(kTestFilePath, kTestContent));
+
+ GDataErrorCode error = GDATA_OTHER_ERROR;
+ GURL upload_url;
+
+ // Initiate uploading a new file to the directory with "parent_resource_id".
+ {
+ base::RunLoop run_loop;
+ drive::InitiateUploadExistingFileRequest* request =
+ new drive::InitiateUploadExistingFileRequest(
+ request_sender_.get(),
+ *url_generator_,
+ kTestContentType,
+ kTestContent.size(),
+ "resource_id", // The resource id of the file to be overwritten.
+ kTestETag,
+ test_util::CreateQuitCallback(
+ &run_loop,
+ test_util::CreateCopyResultCallback(&error, &upload_url)));
+ request_sender_->StartRequestWithRetry(request);
+ run_loop.Run();
+ }
+
+ EXPECT_EQ(HTTP_SUCCESS, error);
+ EXPECT_EQ(kTestUploadExistingFilePath, upload_url.path());
+ EXPECT_EQ(kTestContentType, http_request_.headers["X-Upload-Content-Type"]);
+ EXPECT_EQ(base::Int64ToString(kTestContent.size()),
+ http_request_.headers["X-Upload-Content-Length"]);
+ EXPECT_EQ(kTestETag, http_request_.headers["If-Match"]);
+
+ EXPECT_EQ(net::test_server::METHOD_PUT, http_request_.method);
+ EXPECT_EQ("/upload/drive/v2/files/resource_id?uploadType=resumable",
+ http_request_.relative_url);
+ EXPECT_TRUE(http_request_.has_content);
+ EXPECT_TRUE(http_request_.content.empty());
+
+ // Upload the content to the upload URL.
+ UploadRangeResponse response;
+ scoped_ptr<FileResource> new_entry;
+
+ {
+ base::RunLoop run_loop;
+ drive::ResumeUploadRequest* resume_request =
+ new drive::ResumeUploadRequest(
+ request_sender_.get(),
+ upload_url,
+ 0, // start_position
+ kTestContent.size(), // end_position (exclusive)
+ kTestContent.size(), // content_length,
+ kTestContentType,
+ kTestFilePath,
+ test_util::CreateQuitCallback(
+ &run_loop,
+ test_util::CreateCopyResultCallback(&response, &new_entry)),
+ ProgressCallback());
+ request_sender_->StartRequestWithRetry(resume_request);
+ run_loop.Run();
+ }
+
+ // METHOD_PUT should be used to upload data.
+ EXPECT_EQ(net::test_server::METHOD_PUT, http_request_.method);
+ // Request should go to the upload URL.
+ EXPECT_EQ(upload_url.path(), http_request_.relative_url);
+ // Content-Range header should be added.
+ EXPECT_EQ("bytes 0-" +
+ base::Int64ToString(kTestContent.size() - 1) + "/" +
+ base::Int64ToString(kTestContent.size()),
+ http_request_.headers["Content-Range"]);
+ // The upload content should be set in the HTTP request.
+ EXPECT_TRUE(http_request_.has_content);
+ EXPECT_EQ(kTestContent, http_request_.content);
+
+ // Check the response.
+ EXPECT_EQ(HTTP_SUCCESS, response.code); // Because it's an existing file
+ // The start and end positions should be set to -1, if an upload is complete.
+ EXPECT_EQ(-1, response.start_position_received);
+ EXPECT_EQ(-1, response.end_position_received);
+}
+
+TEST_F(DriveApiRequestsTest, UploadExistingFileRequestWithETagConflicting) {
+ // Set an expected url for uploading.
+ expected_upload_path_ = kTestUploadExistingFilePath;
+
+ // If it turned out that the etag is conflicting, PRECONDITION_FAILED should
+ // be returned.
+ expected_precondition_failed_file_path_ =
+ test_util::GetTestFilePath("drive/error.json");
+
+ const char kTestContentType[] = "text/plain";
+ const std::string kTestContent(100, 'a');
+
+ GDataErrorCode error = GDATA_OTHER_ERROR;
+ GURL upload_url;
+
+ // Initiate uploading a new file to the directory with "parent_resource_id".
+ {
+ base::RunLoop run_loop;
+ drive::InitiateUploadExistingFileRequest* request =
+ new drive::InitiateUploadExistingFileRequest(
+ request_sender_.get(),
+ *url_generator_,
+ kTestContentType,
+ kTestContent.size(),
+ "resource_id", // The resource id of the file to be overwritten.
+ "Conflicting-etag",
+ test_util::CreateQuitCallback(
+ &run_loop,
+ test_util::CreateCopyResultCallback(&error, &upload_url)));
+ request_sender_->StartRequestWithRetry(request);
+ run_loop.Run();
+ }
+
+ EXPECT_EQ(HTTP_PRECONDITION, error);
+ EXPECT_EQ(kTestContentType, http_request_.headers["X-Upload-Content-Type"]);
+ EXPECT_EQ(base::Int64ToString(kTestContent.size()),
+ http_request_.headers["X-Upload-Content-Length"]);
+ EXPECT_EQ("Conflicting-etag", http_request_.headers["If-Match"]);
+
+ EXPECT_EQ(net::test_server::METHOD_PUT, http_request_.method);
+ EXPECT_EQ("/upload/drive/v2/files/resource_id?uploadType=resumable",
+ http_request_.relative_url);
+ EXPECT_TRUE(http_request_.has_content);
+ EXPECT_TRUE(http_request_.content.empty());
+}
+
+TEST_F(DriveApiRequestsTest,
+ UploadExistingFileRequestWithETagConflictOnResumeUpload) {
+ // Set an expected url for uploading.
+ expected_upload_path_ = kTestUploadExistingFilePath;
+
+ const char kTestContentType[] = "text/plain";
+ const std::string kTestContent(100, 'a');
+ const base::FilePath kTestFilePath =
+ temp_dir_.path().AppendASCII("upload_file.txt");
+ ASSERT_TRUE(test_util::WriteStringToFile(kTestFilePath, kTestContent));
+
+ GDataErrorCode error = GDATA_OTHER_ERROR;
+ GURL upload_url;
+
+ // Initiate uploading a new file to the directory with "parent_resource_id".
+ {
+ base::RunLoop run_loop;
+ drive::InitiateUploadExistingFileRequest* request =
+ new drive::InitiateUploadExistingFileRequest(
+ request_sender_.get(),
+ *url_generator_,
+ kTestContentType,
+ kTestContent.size(),
+ "resource_id", // The resource id of the file to be overwritten.
+ kTestETag,
+ test_util::CreateQuitCallback(
+ &run_loop,
+ test_util::CreateCopyResultCallback(&error, &upload_url)));
+ request_sender_->StartRequestWithRetry(request);
+ run_loop.Run();
+ }
+
+ EXPECT_EQ(HTTP_SUCCESS, error);
+ EXPECT_EQ(kTestUploadExistingFilePath, upload_url.path());
+ EXPECT_EQ(kTestContentType, http_request_.headers["X-Upload-Content-Type"]);
+ EXPECT_EQ(base::Int64ToString(kTestContent.size()),
+ http_request_.headers["X-Upload-Content-Length"]);
+ EXPECT_EQ(kTestETag, http_request_.headers["If-Match"]);
+
+ EXPECT_EQ(net::test_server::METHOD_PUT, http_request_.method);
+ EXPECT_EQ("/upload/drive/v2/files/resource_id?uploadType=resumable",
+ http_request_.relative_url);
+ EXPECT_TRUE(http_request_.has_content);
+ EXPECT_TRUE(http_request_.content.empty());
+
+ // Set PRECONDITION_FAILED to the server. This is the emulation of the
+ // confliction during uploading.
+ expected_precondition_failed_file_path_ =
+ test_util::GetTestFilePath("drive/error.json");
+
+ // Upload the content to the upload URL.
+ UploadRangeResponse response;
+ scoped_ptr<FileResource> new_entry;
+
+ {
+ base::RunLoop run_loop;
+ drive::ResumeUploadRequest* resume_request =
+ new drive::ResumeUploadRequest(
+ request_sender_.get(),
+ upload_url,
+ 0, // start_position
+ kTestContent.size(), // end_position (exclusive)
+ kTestContent.size(), // content_length,
+ kTestContentType,
+ kTestFilePath,
+ test_util::CreateQuitCallback(
+ &run_loop,
+ test_util::CreateCopyResultCallback(&response, &new_entry)),
+ ProgressCallback());
+ request_sender_->StartRequestWithRetry(resume_request);
+ run_loop.Run();
+ }
+
+ // METHOD_PUT should be used to upload data.
+ EXPECT_EQ(net::test_server::METHOD_PUT, http_request_.method);
+ // Request should go to the upload URL.
+ EXPECT_EQ(upload_url.path(), http_request_.relative_url);
+ // Content-Range header should be added.
+ EXPECT_EQ("bytes 0-" +
+ base::Int64ToString(kTestContent.size() - 1) + "/" +
+ base::Int64ToString(kTestContent.size()),
+ http_request_.headers["Content-Range"]);
+ // The upload content should be set in the HTTP request.
+ EXPECT_TRUE(http_request_.has_content);
+ EXPECT_EQ(kTestContent, http_request_.content);
+
+ // Check the response.
+ EXPECT_EQ(HTTP_PRECONDITION, response.code);
+ // The start and end positions should be set to -1 for error.
+ EXPECT_EQ(-1, response.start_position_received);
+ EXPECT_EQ(-1, response.end_position_received);
+
+ // New entry should be NULL.
+ EXPECT_FALSE(new_entry.get());
+}
+
+TEST_F(DriveApiRequestsTest, DownloadFileRequest) {
+ const base::FilePath kDownloadedFilePath =
+ temp_dir_.path().AppendASCII("cache_file");
+ const std::string kTestId("dummyId");
+
+ GDataErrorCode result_code = GDATA_OTHER_ERROR;
+ base::FilePath temp_file;
+ {
+ base::RunLoop run_loop;
+ drive::DownloadFileRequest* request = new drive::DownloadFileRequest(
+ request_sender_.get(),
+ *url_generator_,
+ kTestId,
+ kDownloadedFilePath,
+ test_util::CreateQuitCallback(
+ &run_loop,
+ test_util::CreateCopyResultCallback(&result_code, &temp_file)),
+ GetContentCallback(),
+ ProgressCallback());
+ request_sender_->StartRequestWithRetry(request);
+ run_loop.Run();
+ }
+
+ std::string contents;
+ base::ReadFileToString(temp_file, &contents);
+ base::DeleteFile(temp_file, false);
+
+ EXPECT_EQ(HTTP_SUCCESS, result_code);
+ EXPECT_EQ(net::test_server::METHOD_GET, http_request_.method);
+ EXPECT_EQ(kTestDownloadPathPrefix + kTestId, http_request_.relative_url);
+ EXPECT_EQ(kDownloadedFilePath, temp_file);
+
+ const std::string expected_contents = kTestId + kTestId + kTestId;
+ EXPECT_EQ(expected_contents, contents);
+}
+
+TEST_F(DriveApiRequestsTest, DownloadFileRequest_GetContentCallback) {
+ const base::FilePath kDownloadedFilePath =
+ temp_dir_.path().AppendASCII("cache_file");
+ const std::string kTestId("dummyId");
+
+ GDataErrorCode result_code = GDATA_OTHER_ERROR;
+ base::FilePath temp_file;
+ std::string contents;
+ {
+ base::RunLoop run_loop;
+ drive::DownloadFileRequest* request = new drive::DownloadFileRequest(
+ request_sender_.get(),
+ *url_generator_,
+ kTestId,
+ kDownloadedFilePath,
+ test_util::CreateQuitCallback(
+ &run_loop,
+ test_util::CreateCopyResultCallback(&result_code, &temp_file)),
+ base::Bind(&AppendContent, &contents),
+ ProgressCallback());
+ request_sender_->StartRequestWithRetry(request);
+ run_loop.Run();
+ }
+
+ base::DeleteFile(temp_file, false);
+
+ EXPECT_EQ(HTTP_SUCCESS, result_code);
+ EXPECT_EQ(net::test_server::METHOD_GET, http_request_.method);
+ EXPECT_EQ(kTestDownloadPathPrefix + kTestId, http_request_.relative_url);
+ EXPECT_EQ(kDownloadedFilePath, temp_file);
+
+ const std::string expected_contents = kTestId + kTestId + kTestId;
+ EXPECT_EQ(expected_contents, contents);
+}
+
+} // namespace google_apis
diff --git a/chromium/google_apis/drive/drive_api_url_generator.cc b/chromium/google_apis/drive/drive_api_url_generator.cc
new file mode 100644
index 00000000000..c12b947d3dc
--- /dev/null
+++ b/chromium/google_apis/drive/drive_api_url_generator.cc
@@ -0,0 +1,183 @@
+// Copyright (c) 2012 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "google_apis/drive/drive_api_url_generator.h"
+
+#include "base/logging.h"
+#include "base/strings/string_number_conversions.h"
+#include "base/strings/stringprintf.h"
+#include "net/base/escape.h"
+#include "net/base/url_util.h"
+
+namespace google_apis {
+
+namespace {
+
+// Hard coded URLs for communication with a google drive server.
+const char kDriveV2AboutUrl[] = "/drive/v2/about";
+const char kDriveV2AppsUrl[] = "/drive/v2/apps";
+const char kDriveV2ChangelistUrl[] = "/drive/v2/changes";
+const char kDriveV2FilesUrl[] = "/drive/v2/files";
+const char kDriveV2FileUrlPrefix[] = "/drive/v2/files/";
+const char kDriveV2ChildrenUrlFormat[] = "/drive/v2/files/%s/children";
+const char kDriveV2ChildrenUrlForRemovalFormat[] =
+ "/drive/v2/files/%s/children/%s";
+const char kDriveV2FileCopyUrlFormat[] = "/drive/v2/files/%s/copy";
+const char kDriveV2FileDeleteUrlFormat[] = "/drive/v2/files/%s";
+const char kDriveV2FileTrashUrlFormat[] = "/drive/v2/files/%s/trash";
+const char kDriveV2InitiateUploadNewFileUrl[] = "/upload/drive/v2/files";
+const char kDriveV2InitiateUploadExistingFileUrlPrefix[] =
+ "/upload/drive/v2/files/";
+
+GURL AddResumableUploadParam(const GURL& url) {
+ return net::AppendOrReplaceQueryParameter(url, "uploadType", "resumable");
+}
+
+} // namespace
+
+DriveApiUrlGenerator::DriveApiUrlGenerator(const GURL& base_url,
+ const GURL& base_download_url)
+ : base_url_(base_url),
+ base_download_url_(base_download_url) {
+ // Do nothing.
+}
+
+DriveApiUrlGenerator::~DriveApiUrlGenerator() {
+ // Do nothing.
+}
+
+const char DriveApiUrlGenerator::kBaseUrlForProduction[] =
+ "https://www.googleapis.com";
+const char DriveApiUrlGenerator::kBaseDownloadUrlForProduction[] =
+ "https://www.googledrive.com/host/";
+
+GURL DriveApiUrlGenerator::GetAboutGetUrl() const {
+ return base_url_.Resolve(kDriveV2AboutUrl);
+}
+
+GURL DriveApiUrlGenerator::GetAppsListUrl() const {
+ return base_url_.Resolve(kDriveV2AppsUrl);
+}
+
+GURL DriveApiUrlGenerator::GetFilesGetUrl(const std::string& file_id) const {
+ return base_url_.Resolve(kDriveV2FileUrlPrefix + net::EscapePath(file_id));
+}
+
+GURL DriveApiUrlGenerator::GetFilesInsertUrl() const {
+ return base_url_.Resolve(kDriveV2FilesUrl);
+}
+
+GURL DriveApiUrlGenerator::GetFilesPatchUrl(const std::string& file_id,
+ bool set_modified_date,
+ bool update_viewed_date) const {
+ GURL url =
+ base_url_.Resolve(kDriveV2FileUrlPrefix + net::EscapePath(file_id));
+
+ // setModifiedDate is "false" by default.
+ if (set_modified_date)
+ url = net::AppendOrReplaceQueryParameter(url, "setModifiedDate", "true");
+
+ // updateViewedDate is "true" by default.
+ if (!update_viewed_date)
+ url = net::AppendOrReplaceQueryParameter(url, "updateViewedDate", "false");
+
+ return url;
+}
+
+GURL DriveApiUrlGenerator::GetFilesCopyUrl(const std::string& file_id) const {
+ return base_url_.Resolve(base::StringPrintf(
+ kDriveV2FileCopyUrlFormat, net::EscapePath(file_id).c_str()));
+}
+
+GURL DriveApiUrlGenerator::GetFilesListUrl(int max_results,
+ const std::string& page_token,
+ const std::string& q) const {
+ GURL url = base_url_.Resolve(kDriveV2FilesUrl);
+
+ // maxResults is 100 by default.
+ if (max_results != 100) {
+ url = net::AppendOrReplaceQueryParameter(
+ url, "maxResults", base::IntToString(max_results));
+ }
+
+ if (!page_token.empty())
+ url = net::AppendOrReplaceQueryParameter(url, "pageToken", page_token);
+
+ if (!q.empty())
+ url = net::AppendOrReplaceQueryParameter(url, "q", q);
+
+ return url;
+}
+
+GURL DriveApiUrlGenerator::GetFilesDeleteUrl(const std::string& file_id) const {
+ return base_url_.Resolve(base::StringPrintf(
+ kDriveV2FileDeleteUrlFormat, net::EscapePath(file_id).c_str()));
+}
+
+GURL DriveApiUrlGenerator::GetFilesTrashUrl(const std::string& file_id) const {
+ return base_url_.Resolve(base::StringPrintf(
+ kDriveV2FileTrashUrlFormat, net::EscapePath(file_id).c_str()));
+}
+
+GURL DriveApiUrlGenerator::GetChangesListUrl(bool include_deleted,
+ int max_results,
+ const std::string& page_token,
+ int64 start_change_id) const {
+ DCHECK_GE(start_change_id, 0);
+
+ GURL url = base_url_.Resolve(kDriveV2ChangelistUrl);
+
+ // includeDeleted is "true" by default.
+ if (!include_deleted)
+ url = net::AppendOrReplaceQueryParameter(url, "includeDeleted", "false");
+
+ // maxResults is "100" by default.
+ if (max_results != 100) {
+ url = net::AppendOrReplaceQueryParameter(
+ url, "maxResults", base::IntToString(max_results));
+ }
+
+ if (!page_token.empty())
+ url = net::AppendOrReplaceQueryParameter(url, "pageToken", page_token);
+
+ if (start_change_id > 0)
+ url = net::AppendOrReplaceQueryParameter(
+ url, "startChangeId", base::Int64ToString(start_change_id));
+
+ return url;
+}
+
+GURL DriveApiUrlGenerator::GetChildrenInsertUrl(
+ const std::string& file_id) const {
+ return base_url_.Resolve(base::StringPrintf(
+ kDriveV2ChildrenUrlFormat, net::EscapePath(file_id).c_str()));
+}
+
+GURL DriveApiUrlGenerator::GetChildrenDeleteUrl(
+ const std::string& child_id, const std::string& folder_id) const {
+ return base_url_.Resolve(
+ base::StringPrintf(kDriveV2ChildrenUrlForRemovalFormat,
+ net::EscapePath(folder_id).c_str(),
+ net::EscapePath(child_id).c_str()));
+}
+
+GURL DriveApiUrlGenerator::GetInitiateUploadNewFileUrl() const {
+ return AddResumableUploadParam(
+ base_url_.Resolve(kDriveV2InitiateUploadNewFileUrl));
+}
+
+GURL DriveApiUrlGenerator::GetInitiateUploadExistingFileUrl(
+ const std::string& resource_id) const {
+ const GURL& url = base_url_.Resolve(
+ kDriveV2InitiateUploadExistingFileUrlPrefix +
+ net::EscapePath(resource_id));
+ return AddResumableUploadParam(url);
+}
+
+GURL DriveApiUrlGenerator::GenerateDownloadFileUrl(
+ const std::string& resource_id) const {
+ return base_download_url_.Resolve(net::EscapePath(resource_id));
+}
+
+} // namespace google_apis
diff --git a/chromium/google_apis/drive/drive_api_url_generator.h b/chromium/google_apis/drive/drive_api_url_generator.h
new file mode 100644
index 00000000000..cf93edd3255
--- /dev/null
+++ b/chromium/google_apis/drive/drive_api_url_generator.h
@@ -0,0 +1,93 @@
+// Copyright (c) 2013 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef GOOGLE_APIS_DRIVE_DRIVE_API_URL_GENERATOR_H_
+#define GOOGLE_APIS_DRIVE_DRIVE_API_URL_GENERATOR_H_
+
+#include <string>
+
+#include "url/gurl.h"
+
+namespace google_apis {
+
+// This class is used to generate URLs for communicating with drive api
+// servers for production, and a local server for testing.
+class DriveApiUrlGenerator {
+ public:
+ // |base_url| is the path to the target drive api server.
+ // Note that this is an injecting point for a testing server.
+ DriveApiUrlGenerator(const GURL& base_url, const GURL& base_download_url);
+ ~DriveApiUrlGenerator();
+
+ // The base URL for communicating with the production drive api server.
+ static const char kBaseUrlForProduction[];
+
+ // The base URL for the file download server for production.
+ static const char kBaseDownloadUrlForProduction[];
+
+ // Returns a URL to invoke "About: get" method.
+ GURL GetAboutGetUrl() const;
+
+ // Returns a URL to invoke "Apps: list" method.
+ GURL GetAppsListUrl() const;
+
+ // Returns a URL to fetch a file metadata.
+ GURL GetFilesGetUrl(const std::string& file_id) const;
+
+ // Returns a URL to create a resource.
+ GURL GetFilesInsertUrl() const;
+
+ // Returns a URL to patch file metadata.
+ GURL GetFilesPatchUrl(const std::string& file_id,
+ bool set_modified_date,
+ bool update_viewed_date) const;
+
+ // Returns a URL to copy a resource specified by |file_id|.
+ GURL GetFilesCopyUrl(const std::string& file_id) const;
+
+ // Returns a URL to fetch file list.
+ GURL GetFilesListUrl(int max_results,
+ const std::string& page_token,
+ const std::string& q) const;
+
+ // Returns a URL to delete a resource with the given |file_id|.
+ GURL GetFilesDeleteUrl(const std::string& file_id) const;
+
+ // Returns a URL to trash a resource with the given |file_id|.
+ GURL GetFilesTrashUrl(const std::string& file_id) const;
+
+ // Returns a URL to fetch a list of changes.
+ GURL GetChangesListUrl(bool include_deleted,
+ int max_results,
+ const std::string& page_token,
+ int64 start_change_id) const;
+
+ // Returns a URL to add a resource to a directory with |folder_id|.
+ GURL GetChildrenInsertUrl(const std::string& folder_id) const;
+
+ // Returns a URL to remove a resource with |child_id| from a directory
+ // with |folder_id|.
+ GURL GetChildrenDeleteUrl(const std::string& child_id,
+ const std::string& folder_id) const;
+
+ // Returns a URL to initiate uploading a new file.
+ GURL GetInitiateUploadNewFileUrl() const;
+
+ // Returns a URL to initiate uploading an existing file specified by
+ // |resource_id|.
+ GURL GetInitiateUploadExistingFileUrl(const std::string& resource_id) const;
+
+ // Generates a URL for downloading a file.
+ GURL GenerateDownloadFileUrl(const std::string& resource_id) const;
+
+ private:
+ const GURL base_url_;
+ const GURL base_download_url_;
+
+ // This class is copyable hence no DISALLOW_COPY_AND_ASSIGN here.
+};
+
+} // namespace google_apis
+
+#endif // GOOGLE_APIS_DRIVE_DRIVE_API_URL_GENERATOR_H_
diff --git a/chromium/google_apis/drive/drive_api_url_generator_unittest.cc b/chromium/google_apis/drive/drive_api_url_generator_unittest.cc
new file mode 100644
index 00000000000..343b2791c4e
--- /dev/null
+++ b/chromium/google_apis/drive/drive_api_url_generator_unittest.cc
@@ -0,0 +1,394 @@
+// Copyright (c) 2013 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "google_apis/drive/drive_api_url_generator.h"
+
+#include "google_apis/drive/test_util.h"
+#include "testing/gtest/include/gtest/gtest.h"
+#include "url/gurl.h"
+
+namespace google_apis {
+
+class DriveApiUrlGeneratorTest : public testing::Test {
+ public:
+ DriveApiUrlGeneratorTest()
+ : url_generator_(
+ GURL(DriveApiUrlGenerator::kBaseUrlForProduction),
+ GURL(DriveApiUrlGenerator::kBaseDownloadUrlForProduction)),
+ test_url_generator_(
+ test_util::GetBaseUrlForTesting(12345),
+ test_util::GetBaseUrlForTesting(12345).Resolve("download/")) {
+ }
+
+ protected:
+ DriveApiUrlGenerator url_generator_;
+ DriveApiUrlGenerator test_url_generator_;
+};
+
+// Make sure the hard-coded urls are returned.
+TEST_F(DriveApiUrlGeneratorTest, GetAboutGetUrl) {
+ EXPECT_EQ("https://www.googleapis.com/drive/v2/about",
+ url_generator_.GetAboutGetUrl().spec());
+ EXPECT_EQ("http://127.0.0.1:12345/drive/v2/about",
+ test_url_generator_.GetAboutGetUrl().spec());
+}
+
+TEST_F(DriveApiUrlGeneratorTest, GetAppsListUrl) {
+ EXPECT_EQ("https://www.googleapis.com/drive/v2/apps",
+ url_generator_.GetAppsListUrl().spec());
+ EXPECT_EQ("http://127.0.0.1:12345/drive/v2/apps",
+ test_url_generator_.GetAppsListUrl().spec());
+}
+
+TEST_F(DriveApiUrlGeneratorTest, GetFilesGetUrl) {
+ // |file_id| should be embedded into the url.
+ EXPECT_EQ("https://www.googleapis.com/drive/v2/files/0ADK06pfg",
+ url_generator_.GetFilesGetUrl("0ADK06pfg").spec());
+ EXPECT_EQ("https://www.googleapis.com/drive/v2/files/0Bz0bd074",
+ url_generator_.GetFilesGetUrl("0Bz0bd074").spec());
+ EXPECT_EQ("https://www.googleapis.com/drive/v2/files/file%3Afile_id",
+ url_generator_.GetFilesGetUrl("file:file_id").spec());
+
+ EXPECT_EQ("http://127.0.0.1:12345/drive/v2/files/0ADK06pfg",
+ test_url_generator_.GetFilesGetUrl("0ADK06pfg").spec());
+ EXPECT_EQ("http://127.0.0.1:12345/drive/v2/files/0Bz0bd074",
+ test_url_generator_.GetFilesGetUrl("0Bz0bd074").spec());
+ EXPECT_EQ("http://127.0.0.1:12345/drive/v2/files/file%3Afile_id",
+ test_url_generator_.GetFilesGetUrl("file:file_id").spec());
+}
+
+TEST_F(DriveApiUrlGeneratorTest, GetFilesInsertUrl) {
+ EXPECT_EQ("https://www.googleapis.com/drive/v2/files",
+ url_generator_.GetFilesInsertUrl().spec());
+ EXPECT_EQ("http://127.0.0.1:12345/drive/v2/files",
+ test_url_generator_.GetFilesInsertUrl().spec());
+}
+
+TEST_F(DriveApiUrlGeneratorTest, GetFilePatchUrl) {
+ struct TestPattern {
+ bool set_modified_date;
+ bool update_viewed_date;
+ const std::string expected_query;
+ };
+ const TestPattern kTestPatterns[] = {
+ { false, true, "" },
+ { true, true, "?setModifiedDate=true" },
+ { false, false, "?updateViewedDate=false" },
+ { true, false, "?setModifiedDate=true&updateViewedDate=false" },
+ };
+
+ for (size_t i = 0; i < ARRAYSIZE_UNSAFE(kTestPatterns); ++i) {
+ EXPECT_EQ(
+ "https://www.googleapis.com/drive/v2/files/0ADK06pfg" +
+ kTestPatterns[i].expected_query,
+ url_generator_.GetFilesPatchUrl(
+ "0ADK06pfg",
+ kTestPatterns[i].set_modified_date,
+ kTestPatterns[i].update_viewed_date).spec());
+
+ EXPECT_EQ(
+ "https://www.googleapis.com/drive/v2/files/0Bz0bd074" +
+ kTestPatterns[i].expected_query,
+ url_generator_.GetFilesPatchUrl(
+ "0Bz0bd074",
+ kTestPatterns[i].set_modified_date,
+ kTestPatterns[i].update_viewed_date).spec());
+
+ EXPECT_EQ(
+ "https://www.googleapis.com/drive/v2/files/file%3Afile_id" +
+ kTestPatterns[i].expected_query,
+ url_generator_.GetFilesPatchUrl(
+ "file:file_id",
+ kTestPatterns[i].set_modified_date,
+ kTestPatterns[i].update_viewed_date).spec());
+
+
+ EXPECT_EQ(
+ "http://127.0.0.1:12345/drive/v2/files/0ADK06pfg" +
+ kTestPatterns[i].expected_query,
+ test_url_generator_.GetFilesPatchUrl(
+ "0ADK06pfg",
+ kTestPatterns[i].set_modified_date,
+ kTestPatterns[i].update_viewed_date).spec());
+
+ EXPECT_EQ(
+ "http://127.0.0.1:12345/drive/v2/files/0Bz0bd074" +
+ kTestPatterns[i].expected_query,
+ test_url_generator_.GetFilesPatchUrl(
+ "0Bz0bd074",
+ kTestPatterns[i].set_modified_date,
+ kTestPatterns[i].update_viewed_date).spec());
+
+ EXPECT_EQ(
+ "http://127.0.0.1:12345/drive/v2/files/file%3Afile_id" +
+ kTestPatterns[i].expected_query,
+ test_url_generator_.GetFilesPatchUrl(
+ "file:file_id",
+ kTestPatterns[i].set_modified_date,
+ kTestPatterns[i].update_viewed_date).spec());
+ }
+}
+
+TEST_F(DriveApiUrlGeneratorTest, GetFilesCopyUrl) {
+ // |file_id| should be embedded into the url.
+ EXPECT_EQ("https://www.googleapis.com/drive/v2/files/0ADK06pfg/copy",
+ url_generator_.GetFilesCopyUrl("0ADK06pfg").spec());
+ EXPECT_EQ("https://www.googleapis.com/drive/v2/files/0Bz0bd074/copy",
+ url_generator_.GetFilesCopyUrl("0Bz0bd074").spec());
+ EXPECT_EQ("https://www.googleapis.com/drive/v2/files/file%3Afile_id/copy",
+ url_generator_.GetFilesCopyUrl("file:file_id").spec());
+
+ EXPECT_EQ("http://127.0.0.1:12345/drive/v2/files/0ADK06pfg/copy",
+ test_url_generator_.GetFilesCopyUrl("0ADK06pfg").spec());
+ EXPECT_EQ("http://127.0.0.1:12345/drive/v2/files/0Bz0bd074/copy",
+ test_url_generator_.GetFilesCopyUrl("0Bz0bd074").spec());
+ EXPECT_EQ("http://127.0.0.1:12345/drive/v2/files/file%3Afile_id/copy",
+ test_url_generator_.GetFilesCopyUrl("file:file_id").spec());
+}
+
+TEST_F(DriveApiUrlGeneratorTest, GetFilesListUrl) {
+ struct TestPattern {
+ int max_results;
+ const std::string page_token;
+ const std::string q;
+ const std::string expected_query;
+ };
+ const TestPattern kTestPatterns[] = {
+ { 100, "", "", "" },
+ { 150, "", "", "?maxResults=150" },
+ { 10, "", "", "?maxResults=10" },
+ { 100, "token", "", "?pageToken=token" },
+ { 150, "token", "", "?maxResults=150&pageToken=token" },
+ { 10, "token", "", "?maxResults=10&pageToken=token" },
+ { 100, "", "query", "?q=query" },
+ { 150, "", "query", "?maxResults=150&q=query" },
+ { 10, "", "query", "?maxResults=10&q=query" },
+ { 100, "token", "query", "?pageToken=token&q=query" },
+ { 150, "token", "query", "?maxResults=150&pageToken=token&q=query" },
+ { 10, "token", "query", "?maxResults=10&pageToken=token&q=query" },
+ };
+
+ for (size_t i = 0; i < ARRAYSIZE_UNSAFE(kTestPatterns); ++i) {
+ EXPECT_EQ(
+ "https://www.googleapis.com/drive/v2/files" +
+ kTestPatterns[i].expected_query,
+ url_generator_.GetFilesListUrl(
+ kTestPatterns[i].max_results, kTestPatterns[i].page_token,
+ kTestPatterns[i].q).spec());
+
+ EXPECT_EQ(
+ "http://127.0.0.1:12345/drive/v2/files" +
+ kTestPatterns[i].expected_query,
+ test_url_generator_.GetFilesListUrl(
+ kTestPatterns[i].max_results, kTestPatterns[i].page_token,
+ kTestPatterns[i].q).spec());
+ }
+}
+
+TEST_F(DriveApiUrlGeneratorTest, GetFilesDeleteUrl) {
+ // |file_id| should be embedded into the url.
+ EXPECT_EQ("https://www.googleapis.com/drive/v2/files/0ADK06pfg",
+ url_generator_.GetFilesDeleteUrl("0ADK06pfg").spec());
+ EXPECT_EQ("https://www.googleapis.com/drive/v2/files/0Bz0bd074",
+ url_generator_.GetFilesDeleteUrl("0Bz0bd074").spec());
+ EXPECT_EQ("https://www.googleapis.com/drive/v2/files/file%3Afile_id",
+ url_generator_.GetFilesDeleteUrl("file:file_id").spec());
+
+ EXPECT_EQ("http://127.0.0.1:12345/drive/v2/files/0ADK06pfg",
+ test_url_generator_.GetFilesDeleteUrl("0ADK06pfg").spec());
+ EXPECT_EQ("http://127.0.0.1:12345/drive/v2/files/0Bz0bd074",
+ test_url_generator_.GetFilesDeleteUrl("0Bz0bd074").spec());
+ EXPECT_EQ("http://127.0.0.1:12345/drive/v2/files/file%3Afile_id",
+ test_url_generator_.GetFilesDeleteUrl("file:file_id").spec());
+}
+
+TEST_F(DriveApiUrlGeneratorTest, GetFilesTrashUrl) {
+ // |file_id| should be embedded into the url.
+ EXPECT_EQ("https://www.googleapis.com/drive/v2/files/0ADK06pfg/trash",
+ url_generator_.GetFilesTrashUrl("0ADK06pfg").spec());
+ EXPECT_EQ("https://www.googleapis.com/drive/v2/files/0Bz0bd074/trash",
+ url_generator_.GetFilesTrashUrl("0Bz0bd074").spec());
+ EXPECT_EQ("https://www.googleapis.com/drive/v2/files/file%3Afile_id/trash",
+ url_generator_.GetFilesTrashUrl("file:file_id").spec());
+
+ EXPECT_EQ("http://127.0.0.1:12345/drive/v2/files/0ADK06pfg/trash",
+ test_url_generator_.GetFilesTrashUrl("0ADK06pfg").spec());
+ EXPECT_EQ("http://127.0.0.1:12345/drive/v2/files/0Bz0bd074/trash",
+ test_url_generator_.GetFilesTrashUrl("0Bz0bd074").spec());
+ EXPECT_EQ("http://127.0.0.1:12345/drive/v2/files/file%3Afile_id/trash",
+ test_url_generator_.GetFilesTrashUrl("file:file_id").spec());
+}
+
+TEST_F(DriveApiUrlGeneratorTest, GetChangesListUrl) {
+ struct TestPattern {
+ bool include_deleted;
+ int max_results;
+ const std::string page_token;
+ int64 start_change_id;
+ const std::string expected_query;
+ };
+ const TestPattern kTestPatterns[] = {
+ { true, 100, "", 0, "" },
+ { false, 100, "", 0, "?includeDeleted=false" },
+ { true, 150, "", 0, "?maxResults=150" },
+ { false, 150, "", 0, "?includeDeleted=false&maxResults=150" },
+ { true, 10, "", 0, "?maxResults=10" },
+ { false, 10, "", 0, "?includeDeleted=false&maxResults=10" },
+
+ { true, 100, "token", 0, "?pageToken=token" },
+ { false, 100, "token", 0, "?includeDeleted=false&pageToken=token" },
+ { true, 150, "token", 0, "?maxResults=150&pageToken=token" },
+ { false, 150, "token", 0,
+ "?includeDeleted=false&maxResults=150&pageToken=token" },
+ { true, 10, "token", 0, "?maxResults=10&pageToken=token" },
+ { false, 10, "token", 0,
+ "?includeDeleted=false&maxResults=10&pageToken=token" },
+
+ { true, 100, "", 12345, "?startChangeId=12345" },
+ { false, 100, "", 12345, "?includeDeleted=false&startChangeId=12345" },
+ { true, 150, "", 12345, "?maxResults=150&startChangeId=12345" },
+ { false, 150, "", 12345,
+ "?includeDeleted=false&maxResults=150&startChangeId=12345" },
+ { true, 10, "", 12345, "?maxResults=10&startChangeId=12345" },
+ { false, 10, "", 12345,
+ "?includeDeleted=false&maxResults=10&startChangeId=12345" },
+
+ { true, 100, "token", 12345, "?pageToken=token&startChangeId=12345" },
+ { false, 100, "token", 12345,
+ "?includeDeleted=false&pageToken=token&startChangeId=12345" },
+ { true, 150, "token", 12345,
+ "?maxResults=150&pageToken=token&startChangeId=12345" },
+ { false, 150, "token", 12345,
+ "?includeDeleted=false&maxResults=150&pageToken=token"
+ "&startChangeId=12345" },
+ { true, 10, "token", 12345,
+ "?maxResults=10&pageToken=token&startChangeId=12345" },
+ { false, 10, "token", 12345,
+ "?includeDeleted=false&maxResults=10&pageToken=token"
+ "&startChangeId=12345" },
+ };
+
+ for (size_t i = 0; i < ARRAYSIZE_UNSAFE(kTestPatterns); ++i) {
+ EXPECT_EQ(
+ "https://www.googleapis.com/drive/v2/changes" +
+ kTestPatterns[i].expected_query,
+ url_generator_.GetChangesListUrl(
+ kTestPatterns[i].include_deleted,
+ kTestPatterns[i].max_results,
+ kTestPatterns[i].page_token,
+ kTestPatterns[i].start_change_id).spec());
+
+ EXPECT_EQ(
+ "http://127.0.0.1:12345/drive/v2/changes" +
+ kTestPatterns[i].expected_query,
+ test_url_generator_.GetChangesListUrl(
+ kTestPatterns[i].include_deleted,
+ kTestPatterns[i].max_results,
+ kTestPatterns[i].page_token,
+ kTestPatterns[i].start_change_id).spec());
+ }
+}
+
+TEST_F(DriveApiUrlGeneratorTest, GetChildrenInsertUrl) {
+ // |file_id| should be embedded into the url.
+ EXPECT_EQ("https://www.googleapis.com/drive/v2/files/0ADK06pfg/children",
+ url_generator_.GetChildrenInsertUrl("0ADK06pfg").spec());
+ EXPECT_EQ("https://www.googleapis.com/drive/v2/files/0Bz0bd074/children",
+ url_generator_.GetChildrenInsertUrl("0Bz0bd074").spec());
+ EXPECT_EQ(
+ "https://www.googleapis.com/drive/v2/files/file%3Afolder_id/children",
+ url_generator_.GetChildrenInsertUrl("file:folder_id").spec());
+
+ EXPECT_EQ("http://127.0.0.1:12345/drive/v2/files/0ADK06pfg/children",
+ test_url_generator_.GetChildrenInsertUrl("0ADK06pfg").spec());
+ EXPECT_EQ("http://127.0.0.1:12345/drive/v2/files/0Bz0bd074/children",
+ test_url_generator_.GetChildrenInsertUrl("0Bz0bd074").spec());
+ EXPECT_EQ("http://127.0.0.1:12345/drive/v2/files/file%3Afolder_id/children",
+ test_url_generator_.GetChildrenInsertUrl("file:folder_id").spec());
+}
+
+TEST_F(DriveApiUrlGeneratorTest, GetChildrenDeleteUrl) {
+ // |file_id| should be embedded into the url.
+ EXPECT_EQ(
+ "https://www.googleapis.com/drive/v2/files/0ADK06pfg/children/0Bz0bd074",
+ url_generator_.GetChildrenDeleteUrl("0Bz0bd074", "0ADK06pfg").spec());
+ EXPECT_EQ(
+ "https://www.googleapis.com/drive/v2/files/0Bz0bd074/children/0ADK06pfg",
+ url_generator_.GetChildrenDeleteUrl("0ADK06pfg", "0Bz0bd074").spec());
+ EXPECT_EQ(
+ "https://www.googleapis.com/drive/v2/files/file%3Afolder_id/children"
+ "/file%3Achild_id",
+ url_generator_.GetChildrenDeleteUrl(
+ "file:child_id", "file:folder_id").spec());
+
+ EXPECT_EQ(
+ "http://127.0.0.1:12345/drive/v2/files/0ADK06pfg/children/0Bz0bd074",
+ test_url_generator_.GetChildrenDeleteUrl(
+ "0Bz0bd074", "0ADK06pfg").spec());
+ EXPECT_EQ(
+ "http://127.0.0.1:12345/drive/v2/files/0Bz0bd074/children/0ADK06pfg",
+ test_url_generator_.GetChildrenDeleteUrl(
+ "0ADK06pfg", "0Bz0bd074").spec());
+ EXPECT_EQ(
+ "http://127.0.0.1:12345/drive/v2/files/file%3Afolder_id/children/"
+ "file%3Achild_id",
+ test_url_generator_.GetChildrenDeleteUrl(
+ "file:child_id", "file:folder_id").spec());
+}
+
+TEST_F(DriveApiUrlGeneratorTest, GetInitiateUploadNewFileUrl) {
+ EXPECT_EQ(
+ "https://www.googleapis.com/upload/drive/v2/files?uploadType=resumable",
+ url_generator_.GetInitiateUploadNewFileUrl().spec());
+
+ EXPECT_EQ(
+ "http://127.0.0.1:12345/upload/drive/v2/files?uploadType=resumable",
+ test_url_generator_.GetInitiateUploadNewFileUrl().spec());
+}
+
+TEST_F(DriveApiUrlGeneratorTest, GetInitiateUploadExistingFileUrl) {
+ // |resource_id| should be embedded into the url.
+ EXPECT_EQ(
+ "https://www.googleapis.com/upload/drive/v2/files/0ADK06pfg"
+ "?uploadType=resumable",
+ url_generator_.GetInitiateUploadExistingFileUrl("0ADK06pfg").spec());
+ EXPECT_EQ(
+ "https://www.googleapis.com/upload/drive/v2/files/0Bz0bd074"
+ "?uploadType=resumable",
+ url_generator_.GetInitiateUploadExistingFileUrl("0Bz0bd074").spec());
+ EXPECT_EQ(
+ "https://www.googleapis.com/upload/drive/v2/files/file%3Afile_id"
+ "?uploadType=resumable",
+ url_generator_.GetInitiateUploadExistingFileUrl("file:file_id").spec());
+
+ EXPECT_EQ(
+ "http://127.0.0.1:12345/upload/drive/v2/files/0ADK06pfg"
+ "?uploadType=resumable",
+ test_url_generator_.GetInitiateUploadExistingFileUrl(
+ "0ADK06pfg").spec());
+ EXPECT_EQ(
+ "http://127.0.0.1:12345/upload/drive/v2/files/0Bz0bd074"
+ "?uploadType=resumable",
+ test_url_generator_.GetInitiateUploadExistingFileUrl(
+ "0Bz0bd074").spec());
+ EXPECT_EQ(
+ "http://127.0.0.1:12345/upload/drive/v2/files/file%3Afile_id"
+ "?uploadType=resumable",
+ test_url_generator_.GetInitiateUploadExistingFileUrl(
+ "file:file_id").spec());
+}
+
+TEST_F(DriveApiUrlGeneratorTest, GenerateDownloadFileUrl) {
+ EXPECT_EQ(
+ "https://www.googledrive.com/host/resourceId",
+ url_generator_.GenerateDownloadFileUrl("resourceId").spec());
+ EXPECT_EQ(
+ "https://www.googledrive.com/host/file%3AresourceId",
+ url_generator_.GenerateDownloadFileUrl("file:resourceId").spec());
+ EXPECT_EQ(
+ "http://127.0.0.1:12345/download/resourceId",
+ test_url_generator_.GenerateDownloadFileUrl("resourceId").spec());
+}
+
+} // namespace google_apis
diff --git a/chromium/google_apis/drive/drive_common_callbacks.h b/chromium/google_apis/drive/drive_common_callbacks.h
new file mode 100644
index 00000000000..c31bea0358e
--- /dev/null
+++ b/chromium/google_apis/drive/drive_common_callbacks.h
@@ -0,0 +1,62 @@
+// Copyright 2013 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+//
+// This file contains callback types used for communicating with the Drive
+// server via WAPI (Documents List API) and Drive API.
+
+#ifndef GOOGLE_APIS_DRIVE_DRIVE_COMMON_CALLBACKS_H_
+#define GOOGLE_APIS_DRIVE_DRIVE_COMMON_CALLBACKS_H_
+
+#include "google_apis/drive/base_requests.h"
+
+namespace google_apis {
+
+class AboutResource;
+class AppList;
+class ResourceEntry;
+class ResourceList;
+
+// Callback used for getting ResourceList.
+typedef base::Callback<void(GDataErrorCode error,
+ scoped_ptr<ResourceList> resource_list)>
+ GetResourceListCallback;
+
+// Callback used for getting ResourceEntry.
+typedef base::Callback<void(GDataErrorCode error,
+ scoped_ptr<ResourceEntry> entry)>
+ GetResourceEntryCallback;
+
+// Callback used for getting AboutResource.
+typedef base::Callback<void(GDataErrorCode error,
+ scoped_ptr<AboutResource> about_resource)>
+ AboutResourceCallback;
+
+// Callback used for getting ShareUrl.
+typedef base::Callback<void(GDataErrorCode error,
+ const GURL& share_url)> GetShareUrlCallback;
+
+// Callback used for getting AppList.
+typedef base::Callback<void(GDataErrorCode error,
+ scoped_ptr<AppList> app_list)> AppListCallback;
+
+// Callback used for handling UploadRangeResponse.
+typedef base::Callback<void(
+ const UploadRangeResponse& response,
+ scoped_ptr<ResourceEntry> new_entry)> UploadRangeCallback;
+
+// Callback used for authorizing an app. |open_url| is used to open the target
+// file with the authorized app.
+typedef base::Callback<void(GDataErrorCode error,
+ const GURL& open_url)>
+ AuthorizeAppCallback;
+
+// Closure for canceling a certain request. Each request-issuing method returns
+// this type of closure. If it is called during the request is in-flight, the
+// callback passed with the request is invoked with GDATA_CANCELLED. If the
+// request is already finished, nothing happens.
+typedef base::Closure CancelCallback;
+
+} // namespace google_apis
+
+#endif // GOOGLE_APIS_DRIVE_DRIVE_COMMON_CALLBACKS_H_
diff --git a/chromium/google_apis/drive/drive_entry_kinds.h b/chromium/google_apis/drive/drive_entry_kinds.h
new file mode 100644
index 00000000000..27e0f69d046
--- /dev/null
+++ b/chromium/google_apis/drive/drive_entry_kinds.h
@@ -0,0 +1,40 @@
+// Copyright (c) 2012 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef GOOGLE_APIS_DRIVE_DRIVE_ENTRY_KINDS_H_
+#define GOOGLE_APIS_DRIVE_DRIVE_ENTRY_KINDS_H_
+
+namespace google_apis {
+
+// DriveEntryKind specifies the kind of a Drive entry.
+//
+// kEntryKindMap in gdata_wapi_parser.cc should also be updated if you modify
+// DriveEntryKind. The compiler will catch if they are not in sync.
+enum DriveEntryKind {
+ ENTRY_KIND_UNKNOWN,
+ // Special entries.
+ ENTRY_KIND_ITEM,
+ ENTRY_KIND_SITE,
+ // Hosted Google document.
+ ENTRY_KIND_DOCUMENT,
+ ENTRY_KIND_SPREADSHEET,
+ ENTRY_KIND_PRESENTATION,
+ ENTRY_KIND_DRAWING,
+ ENTRY_KIND_TABLE,
+ ENTRY_KIND_FORM,
+ // Hosted external application document.
+ ENTRY_KIND_EXTERNAL_APP,
+ // Folders; collections.
+ ENTRY_KIND_FOLDER,
+ // Regular files.
+ ENTRY_KIND_FILE,
+ ENTRY_KIND_PDF,
+
+ // This should be the last item.
+ ENTRY_KIND_MAX_VALUE,
+};
+
+} // namespace google_apis
+
+#endif // GOOGLE_APIS_DRIVE_DRIVE_ENTRY_KINDS_H_
diff --git a/chromium/google_apis/drive/dummy_auth_service.cc b/chromium/google_apis/drive/dummy_auth_service.cc
new file mode 100644
index 00000000000..e1b6891f969
--- /dev/null
+++ b/chromium/google_apis/drive/dummy_auth_service.cc
@@ -0,0 +1,43 @@
+// Copyright 2013 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "google_apis/drive/dummy_auth_service.h"
+
+namespace google_apis {
+
+DummyAuthService::DummyAuthService() {
+ set_access_token("dummy");
+ set_refresh_token("dummy");
+}
+
+void DummyAuthService::AddObserver(AuthServiceObserver* observer) {
+}
+
+void DummyAuthService::RemoveObserver(AuthServiceObserver* observer) {
+}
+
+void DummyAuthService::StartAuthentication(const AuthStatusCallback& callback) {
+}
+
+bool DummyAuthService::HasAccessToken() const {
+ return !access_token_.empty();
+}
+
+bool DummyAuthService::HasRefreshToken() const {
+ return !refresh_token_.empty();
+}
+
+const std::string& DummyAuthService::access_token() const {
+ return access_token_;
+}
+
+void DummyAuthService::ClearAccessToken() {
+ access_token_.clear();
+}
+
+void DummyAuthService::ClearRefreshToken() {
+ refresh_token_.clear();
+}
+
+} // namespace google_apis
diff --git a/chromium/google_apis/drive/dummy_auth_service.h b/chromium/google_apis/drive/dummy_auth_service.h
new file mode 100644
index 00000000000..a69da6b9a5c
--- /dev/null
+++ b/chromium/google_apis/drive/dummy_auth_service.h
@@ -0,0 +1,43 @@
+// Copyright 2013 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef GOOGLE_APIS_DRIVE_DUMMY_AUTH_SERVICE_H_
+#define GOOGLE_APIS_DRIVE_DUMMY_AUTH_SERVICE_H_
+
+#include "base/compiler_specific.h"
+#include "google_apis/drive/auth_service_interface.h"
+
+namespace google_apis {
+
+// Dummy implementation of AuthServiceInterface that always return a dummy
+// access token.
+class DummyAuthService : public AuthServiceInterface {
+ public:
+ // The constructor presets non-empty tokens. When a test for checking auth
+ // failure case (i.e., empty tokens) is needed, explicitly clear them by the
+ // Clear{Access, Refresh}Token methods.
+ DummyAuthService();
+
+ void set_access_token(const std::string& token) { access_token_ = token; }
+ void set_refresh_token(const std::string& token) { refresh_token_ = token; }
+ const std::string& refresh_token() const { return refresh_token_; }
+
+ // AuthServiceInterface overrides.
+ virtual void AddObserver(AuthServiceObserver* observer) OVERRIDE;
+ virtual void RemoveObserver(AuthServiceObserver* observer) OVERRIDE;
+ virtual void StartAuthentication(const AuthStatusCallback& callback) OVERRIDE;
+ virtual bool HasAccessToken() const OVERRIDE;
+ virtual bool HasRefreshToken() const OVERRIDE;
+ virtual const std::string& access_token() const OVERRIDE;
+ virtual void ClearAccessToken() OVERRIDE;
+ virtual void ClearRefreshToken() OVERRIDE;
+
+ private:
+ std::string access_token_;
+ std::string refresh_token_;
+};
+
+} // namespace google_apis
+
+#endif // GOOGLE_APIS_DRIVE_DUMMY_AUTH_SERVICE_H_
diff --git a/chromium/google_apis/drive/gdata_contacts_requests.cc b/chromium/google_apis/drive/gdata_contacts_requests.cc
new file mode 100644
index 00000000000..11419af449b
--- /dev/null
+++ b/chromium/google_apis/drive/gdata_contacts_requests.cc
@@ -0,0 +1,115 @@
+// Copyright (c) 2012 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "google_apis/drive/gdata_contacts_requests.h"
+
+#include "google_apis/drive/time_util.h"
+#include "net/base/url_util.h"
+#include "url/gurl.h"
+
+namespace google_apis {
+
+namespace {
+
+// URL requesting all contact groups.
+const char kGetContactGroupsURL[] =
+ "https://www.google.com/m8/feeds/groups/default/full?alt=json";
+
+// URL requesting all contacts.
+// TODO(derat): Per https://goo.gl/AufHP, "The feed may not contain all of the
+// user's contacts, because there's a default limit on the number of results
+// returned." Decide if 10000 is reasonable or not.
+const char kGetContactsURL[] =
+ "https://www.google.com/m8/feeds/contacts/default/full"
+ "?alt=json&showdeleted=true&max-results=10000";
+
+// Query parameter optionally appended to |kGetContactsURL| to return contacts
+// from a specific group (as opposed to all contacts).
+const char kGetContactsGroupParam[] = "group";
+
+// Query parameter optionally appended to |kGetContactsURL| to return only
+// recently-updated contacts.
+const char kGetContactsUpdatedMinParam[] = "updated-min";
+
+} // namespace
+
+//========================== GetContactGroupsRequest =========================
+
+GetContactGroupsRequest::GetContactGroupsRequest(
+ RequestSender* runner,
+ const GetDataCallback& callback)
+ : GetDataRequest(runner, callback) {
+}
+
+GetContactGroupsRequest::~GetContactGroupsRequest() {}
+
+GURL GetContactGroupsRequest::GetURL() const {
+ return !feed_url_for_testing_.is_empty() ?
+ feed_url_for_testing_ :
+ GURL(kGetContactGroupsURL);
+}
+
+//============================ GetContactsRequest ============================
+
+GetContactsRequest::GetContactsRequest(
+ RequestSender* runner,
+ const std::string& group_id,
+ const base::Time& min_update_time,
+ const GetDataCallback& callback)
+ : GetDataRequest(runner, callback),
+ group_id_(group_id),
+ min_update_time_(min_update_time) {
+}
+
+GetContactsRequest::~GetContactsRequest() {}
+
+GURL GetContactsRequest::GetURL() const {
+ if (!feed_url_for_testing_.is_empty())
+ return GURL(feed_url_for_testing_);
+
+ GURL url(kGetContactsURL);
+
+ if (!group_id_.empty()) {
+ url = net::AppendQueryParameter(url, kGetContactsGroupParam, group_id_);
+ }
+ if (!min_update_time_.is_null()) {
+ std::string time_rfc3339 = util::FormatTimeAsString(min_update_time_);
+ url = net::AppendQueryParameter(
+ url, kGetContactsUpdatedMinParam, time_rfc3339);
+ }
+ return url;
+}
+
+//========================== GetContactPhotoRequest ==========================
+
+GetContactPhotoRequest::GetContactPhotoRequest(
+ RequestSender* runner,
+ const GURL& photo_url,
+ const GetContentCallback& callback)
+ : UrlFetchRequestBase(runner),
+ photo_url_(photo_url),
+ callback_(callback) {
+}
+
+GetContactPhotoRequest::~GetContactPhotoRequest() {}
+
+GURL GetContactPhotoRequest::GetURL() const {
+ return photo_url_;
+}
+
+void GetContactPhotoRequest::ProcessURLFetchResults(
+ const net::URLFetcher* source) {
+ GDataErrorCode code = GetErrorCode();
+ scoped_ptr<std::string> data(new std::string(response_writer()->data()));
+ callback_.Run(code, data.Pass());
+ OnProcessURLFetchResultsComplete();
+}
+
+void GetContactPhotoRequest::RunCallbackOnPrematureFailure(
+ GDataErrorCode code) {
+ scoped_ptr<std::string> data(new std::string);
+ callback_.Run(code, data.Pass());
+}
+
+} // namespace google_apis
diff --git a/chromium/google_apis/drive/gdata_contacts_requests.h b/chromium/google_apis/drive/gdata_contacts_requests.h
new file mode 100644
index 00000000000..05ce693242d
--- /dev/null
+++ b/chromium/google_apis/drive/gdata_contacts_requests.h
@@ -0,0 +1,102 @@
+// Copyright (c) 2012 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef GOOGLE_APIS_DRIVE_GDATA_CONTACTS_REQUESTS_H_
+#define GOOGLE_APIS_DRIVE_GDATA_CONTACTS_REQUESTS_H_
+
+#include <string>
+
+#include "base/time/time.h"
+#include "google_apis/drive/base_requests.h"
+
+namespace google_apis {
+
+//========================== GetContactGroupsRequest =========================
+
+// This class fetches a JSON feed containing a user's contact groups.
+class GetContactGroupsRequest : public GetDataRequest {
+ public:
+ GetContactGroupsRequest(RequestSender* runner,
+ const GetDataCallback& callback);
+ virtual ~GetContactGroupsRequest();
+
+ void set_feed_url_for_testing(const GURL& url) {
+ feed_url_for_testing_ = url;
+ }
+
+ protected:
+ // Overridden from GetDataRequest.
+ virtual GURL GetURL() const OVERRIDE;
+
+ private:
+ // If non-empty, URL of the feed to fetch.
+ GURL feed_url_for_testing_;
+
+ DISALLOW_COPY_AND_ASSIGN(GetContactGroupsRequest);
+};
+
+//============================ GetContactsRequest ============================
+
+// This class fetches a JSON feed containing a user's contacts.
+class GetContactsRequest : public GetDataRequest {
+ public:
+ GetContactsRequest(RequestSender* runner,
+ const std::string& group_id,
+ const base::Time& min_update_time,
+ const GetDataCallback& callback);
+ virtual ~GetContactsRequest();
+
+ void set_feed_url_for_testing(const GURL& url) {
+ feed_url_for_testing_ = url;
+ }
+
+ protected:
+ // Overridden from GetDataRequest.
+ virtual GURL GetURL() const OVERRIDE;
+
+ private:
+ // If non-empty, URL of the feed to fetch.
+ GURL feed_url_for_testing_;
+
+ // If non-empty, contains the ID of the group whose contacts should be
+ // returned. Group IDs generally look like this:
+ // http://www.google.com/m8/feeds/groups/user%40gmail.com/base/6
+ std::string group_id_;
+
+ // If is_null() is false, contains a minimum last-updated time that will be
+ // used to filter contacts.
+ base::Time min_update_time_;
+
+ DISALLOW_COPY_AND_ASSIGN(GetContactsRequest);
+};
+
+//========================== GetContactPhotoRequest ==========================
+
+// This class fetches a contact's photo.
+class GetContactPhotoRequest : public UrlFetchRequestBase {
+ public:
+ GetContactPhotoRequest(RequestSender* runner,
+ const GURL& photo_url,
+ const GetContentCallback& callback);
+ virtual ~GetContactPhotoRequest();
+
+ protected:
+ // Overridden from UrlFetchRequestBase.
+ virtual GURL GetURL() const OVERRIDE;
+ virtual void ProcessURLFetchResults(const net::URLFetcher* source) OVERRIDE;
+ virtual void RunCallbackOnPrematureFailure(GDataErrorCode code) OVERRIDE;
+
+ private:
+ // Location of the photo to fetch.
+ GURL photo_url_;
+
+ // Callback to which the photo data is passed.
+ GetContentCallback callback_;
+
+ DISALLOW_COPY_AND_ASSIGN(GetContactPhotoRequest);
+};
+
+} // namespace google_apis
+
+#endif // GOOGLE_APIS_DRIVE_GDATA_CONTACTS_REQUESTS_H_
diff --git a/chromium/google_apis/drive/gdata_errorcode.cc b/chromium/google_apis/drive/gdata_errorcode.cc
new file mode 100644
index 00000000000..89fa3f35054
--- /dev/null
+++ b/chromium/google_apis/drive/gdata_errorcode.cc
@@ -0,0 +1,93 @@
+// Copyright 2013 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "google_apis/drive/gdata_errorcode.h"
+
+#include "base/logging.h"
+#include "base/strings/string_number_conversions.h"
+
+namespace google_apis {
+
+std::string GDataErrorCodeToString(GDataErrorCode error) {
+ switch (error) {
+ case HTTP_SUCCESS:
+ return"HTTP_SUCCESS";
+
+ case HTTP_CREATED:
+ return"HTTP_CREATED";
+
+ case HTTP_NO_CONTENT:
+ return"HTTP_NO_CONTENT";
+
+ case HTTP_FOUND:
+ return"HTTP_FOUND";
+
+ case HTTP_NOT_MODIFIED:
+ return"HTTP_NOT_MODIFIED";
+
+ case HTTP_RESUME_INCOMPLETE:
+ return"HTTP_RESUME_INCOMPLETE";
+
+ case HTTP_BAD_REQUEST:
+ return"HTTP_BAD_REQUEST";
+
+ case HTTP_UNAUTHORIZED:
+ return"HTTP_UNAUTHORIZED";
+
+ case HTTP_FORBIDDEN:
+ return"HTTP_FORBIDDEN";
+
+ case HTTP_NOT_FOUND:
+ return"HTTP_NOT_FOUND";
+
+ case HTTP_CONFLICT:
+ return"HTTP_CONFLICT";
+
+ case HTTP_GONE:
+ return "HTTP_GONE";
+
+ case HTTP_LENGTH_REQUIRED:
+ return"HTTP_LENGTH_REQUIRED";
+
+ case HTTP_PRECONDITION:
+ return"HTTP_PRECONDITION";
+
+ case HTTP_INTERNAL_SERVER_ERROR:
+ return"HTTP_INTERNAL_SERVER_ERROR";
+
+ case HTTP_NOT_IMPLEMENTED:
+ return "HTTP_NOT_IMPLEMENTED";
+
+ case HTTP_BAD_GATEWAY:
+ return"HTTP_BAD_GATEWAY";
+
+ case HTTP_SERVICE_UNAVAILABLE:
+ return"HTTP_SERVICE_UNAVAILABLE";
+
+ case GDATA_PARSE_ERROR:
+ return"GDATA_PARSE_ERROR";
+
+ case GDATA_FILE_ERROR:
+ return"GDATA_FILE_ERROR";
+
+ case GDATA_CANCELLED:
+ return"GDATA_CANCELLED";
+
+ case GDATA_OTHER_ERROR:
+ return"GDATA_OTHER_ERROR";
+
+ case GDATA_NO_CONNECTION:
+ return"GDATA_NO_CONNECTION";
+
+ case GDATA_NOT_READY:
+ return"GDATA_NOT_READY";
+
+ case GDATA_NO_SPACE:
+ return"GDATA_NO_SPACE";
+ }
+
+ return "UNKNOWN_ERROR_" + base::IntToString(error);
+}
+
+} // namespace google_apis
diff --git a/chromium/google_apis/drive/gdata_errorcode.h b/chromium/google_apis/drive/gdata_errorcode.h
new file mode 100644
index 00000000000..a275cb39164
--- /dev/null
+++ b/chromium/google_apis/drive/gdata_errorcode.h
@@ -0,0 +1,46 @@
+// Copyright (c) 2012 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef GOOGLE_APIS_DRIVE_GDATA_ERRORCODE_H_
+#define GOOGLE_APIS_DRIVE_GDATA_ERRORCODE_H_
+
+#include <string>
+
+namespace google_apis {
+
+// HTTP errors that can be returned by GData service.
+enum GDataErrorCode {
+ HTTP_SUCCESS = 200,
+ HTTP_CREATED = 201,
+ HTTP_NO_CONTENT = 204,
+ HTTP_FOUND = 302,
+ HTTP_NOT_MODIFIED = 304,
+ HTTP_RESUME_INCOMPLETE = 308,
+ HTTP_BAD_REQUEST = 400,
+ HTTP_UNAUTHORIZED = 401,
+ HTTP_FORBIDDEN = 403,
+ HTTP_NOT_FOUND = 404,
+ HTTP_CONFLICT = 409,
+ HTTP_GONE = 410,
+ HTTP_LENGTH_REQUIRED = 411,
+ HTTP_PRECONDITION = 412,
+ HTTP_INTERNAL_SERVER_ERROR = 500,
+ HTTP_NOT_IMPLEMENTED = 501,
+ HTTP_BAD_GATEWAY = 502,
+ HTTP_SERVICE_UNAVAILABLE = 503,
+ GDATA_PARSE_ERROR = -100,
+ GDATA_FILE_ERROR = -101,
+ GDATA_CANCELLED = -102,
+ GDATA_OTHER_ERROR = -103,
+ GDATA_NO_CONNECTION = -104,
+ GDATA_NOT_READY = -105,
+ GDATA_NO_SPACE = -106,
+};
+
+// Returns a string representation of GDataErrorCode.
+std::string GDataErrorCodeToString(GDataErrorCode error);
+
+} // namespace google_apis
+
+#endif // GOOGLE_APIS_DRIVE_GDATA_ERRORCODE_H_
diff --git a/chromium/google_apis/drive/gdata_wapi_parser.cc b/chromium/google_apis/drive/gdata_wapi_parser.cc
new file mode 100644
index 00000000000..82f9d43151f
--- /dev/null
+++ b/chromium/google_apis/drive/gdata_wapi_parser.cc
@@ -0,0 +1,883 @@
+// Copyright (c) 2012 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "google_apis/drive/gdata_wapi_parser.h"
+
+#include <algorithm>
+#include <string>
+
+#include "base/basictypes.h"
+#include "base/files/file_path.h"
+#include "base/json/json_value_converter.h"
+#include "base/memory/scoped_ptr.h"
+#include "base/strings/string_number_conversions.h"
+#include "base/strings/string_piece.h"
+#include "base/strings/string_util.h"
+#include "base/strings/utf_string_conversions.h"
+#include "base/values.h"
+#include "google_apis/drive/time_util.h"
+
+using base::Value;
+using base::DictionaryValue;
+using base::ListValue;
+
+namespace google_apis {
+
+namespace {
+
+// Term values for kSchemeKind category:
+const char kTermPrefix[] = "http://schemas.google.com/docs/2007#";
+
+// Node names.
+const char kEntryNode[] = "entry";
+
+// Field names.
+const char kAuthorField[] = "author";
+const char kCategoryField[] = "category";
+const char kChangestampField[] = "docs$changestamp.value";
+const char kContentField[] = "content";
+const char kDeletedField[] = "gd$deleted";
+const char kETagField[] = "gd$etag";
+const char kEmailField[] = "email.$t";
+const char kEntryField[] = "entry";
+const char kFeedField[] = "feed";
+const char kFeedLinkField[] = "gd$feedLink";
+const char kFileNameField[] = "docs$filename.$t";
+const char kHrefField[] = "href";
+const char kIDField[] = "id.$t";
+const char kInstalledAppField[] = "docs$installedApp";
+const char kInstalledAppNameField[] = "docs$installedAppName";
+const char kInstalledAppIdField[] = "docs$installedAppId";
+const char kInstalledAppIconField[] = "docs$installedAppIcon";
+const char kInstalledAppIconCategoryField[] = "docs$installedAppIconCategory";
+const char kInstalledAppIconSizeField[] = "docs$installedAppIconSize";
+const char kInstalledAppObjectTypeField[] = "docs$installedAppObjectType";
+const char kInstalledAppPrimaryFileExtensionField[] =
+ "docs$installedAppPrimaryFileExtension";
+const char kInstalledAppPrimaryMimeTypeField[] =
+ "docs$installedAppPrimaryMimeType";
+const char kInstalledAppSecondaryFileExtensionField[] =
+ "docs$installedAppSecondaryFileExtension";
+const char kInstalledAppSecondaryMimeTypeField[] =
+ "docs$installedAppSecondaryMimeType";
+const char kInstalledAppSupportsCreateField[] =
+ "docs$installedAppSupportsCreate";
+const char kItemsPerPageField[] = "openSearch$itemsPerPage.$t";
+const char kLabelField[] = "label";
+const char kLargestChangestampField[] = "docs$largestChangestamp.value";
+const char kLastViewedField[] = "gd$lastViewed.$t";
+const char kLinkField[] = "link";
+const char kMD5Field[] = "docs$md5Checksum.$t";
+const char kNameField[] = "name.$t";
+const char kPublishedField[] = "published.$t";
+const char kQuotaBytesTotalField[] = "gd$quotaBytesTotal.$t";
+const char kQuotaBytesUsedField[] = "gd$quotaBytesUsed.$t";
+const char kRelField[] = "rel";
+const char kRemovedField[] = "docs$removed";
+const char kResourceIdField[] = "gd$resourceId.$t";
+const char kSchemeField[] = "scheme";
+const char kSizeField[] = "docs$size.$t";
+const char kSrcField[] = "src";
+const char kStartIndexField[] = "openSearch$startIndex.$t";
+const char kSuggestedFileNameField[] = "docs$suggestedFilename.$t";
+const char kTField[] = "$t";
+const char kTermField[] = "term";
+const char kTitleField[] = "title";
+const char kTitleTField[] = "title.$t";
+const char kTypeField[] = "type";
+const char kUpdatedField[] = "updated.$t";
+
+// Link Prefixes
+const char kOpenWithPrefix[] = "http://schemas.google.com/docs/2007#open-with-";
+const size_t kOpenWithPrefixSize = arraysize(kOpenWithPrefix) - 1;
+
+struct EntryKindMap {
+ DriveEntryKind kind;
+ const char* entry;
+ const char* extension;
+};
+
+const EntryKindMap kEntryKindMap[] = {
+ { ENTRY_KIND_UNKNOWN, "unknown", NULL},
+ { ENTRY_KIND_ITEM, "item", NULL},
+ { ENTRY_KIND_DOCUMENT, "document", ".gdoc"},
+ { ENTRY_KIND_SPREADSHEET, "spreadsheet", ".gsheet"},
+ { ENTRY_KIND_PRESENTATION, "presentation", ".gslides" },
+ { ENTRY_KIND_DRAWING, "drawing", ".gdraw"},
+ { ENTRY_KIND_TABLE, "table", ".gtable"},
+ { ENTRY_KIND_FORM, "form", ".gform"},
+ { ENTRY_KIND_EXTERNAL_APP, "externalapp", ".glink"},
+ { ENTRY_KIND_SITE, "site", NULL},
+ { ENTRY_KIND_FOLDER, "folder", NULL},
+ { ENTRY_KIND_FILE, "file", NULL},
+ { ENTRY_KIND_PDF, "pdf", NULL},
+};
+COMPILE_ASSERT(arraysize(kEntryKindMap) == ENTRY_KIND_MAX_VALUE,
+ EntryKindMap_and_DriveEntryKind_are_not_in_sync);
+
+struct LinkTypeMap {
+ Link::LinkType type;
+ const char* rel;
+};
+
+const LinkTypeMap kLinkTypeMap[] = {
+ { Link::LINK_SELF,
+ "self" },
+ { Link::LINK_NEXT,
+ "next" },
+ { Link::LINK_PARENT,
+ "http://schemas.google.com/docs/2007#parent" },
+ { Link::LINK_ALTERNATE,
+ "alternate"},
+ { Link::LINK_EDIT,
+ "edit" },
+ { Link::LINK_EDIT_MEDIA,
+ "edit-media" },
+ { Link::LINK_ALT_EDIT_MEDIA,
+ "http://schemas.google.com/docs/2007#alt-edit-media" },
+ { Link::LINK_ALT_POST,
+ "http://schemas.google.com/docs/2007#alt-post" },
+ { Link::LINK_FEED,
+ "http://schemas.google.com/g/2005#feed"},
+ { Link::LINK_POST,
+ "http://schemas.google.com/g/2005#post"},
+ { Link::LINK_BATCH,
+ "http://schemas.google.com/g/2005#batch"},
+ { Link::LINK_THUMBNAIL,
+ "http://schemas.google.com/docs/2007/thumbnail"},
+ { Link::LINK_RESUMABLE_EDIT_MEDIA,
+ "http://schemas.google.com/g/2005#resumable-edit-media"},
+ { Link::LINK_RESUMABLE_CREATE_MEDIA,
+ "http://schemas.google.com/g/2005#resumable-create-media"},
+ { Link::LINK_TABLES_FEED,
+ "http://schemas.google.com/spreadsheets/2006#tablesfeed"},
+ { Link::LINK_WORKSHEET_FEED,
+ "http://schemas.google.com/spreadsheets/2006#worksheetsfeed"},
+ { Link::LINK_EMBED,
+ "http://schemas.google.com/docs/2007#embed"},
+ { Link::LINK_PRODUCT,
+ "http://schemas.google.com/docs/2007#product"},
+ { Link::LINK_ICON,
+ "http://schemas.google.com/docs/2007#icon"},
+ { Link::LINK_SHARE,
+ "http://schemas.google.com/docs/2007#share"},
+};
+
+struct ResourceLinkTypeMap {
+ ResourceLink::ResourceLinkType type;
+ const char* rel;
+};
+
+const ResourceLinkTypeMap kFeedLinkTypeMap[] = {
+ { ResourceLink::FEED_LINK_ACL,
+ "http://schemas.google.com/acl/2007#accessControlList" },
+ { ResourceLink::FEED_LINK_REVISIONS,
+ "http://schemas.google.com/docs/2007/revisions" },
+};
+
+struct CategoryTypeMap {
+ Category::CategoryType type;
+ const char* scheme;
+};
+
+const CategoryTypeMap kCategoryTypeMap[] = {
+ { Category::CATEGORY_KIND, "http://schemas.google.com/g/2005#kind" },
+ { Category::CATEGORY_LABEL, "http://schemas.google.com/g/2005/labels" },
+};
+
+struct AppIconCategoryMap {
+ AppIcon::IconCategory category;
+ const char* category_name;
+};
+
+const AppIconCategoryMap kAppIconCategoryMap[] = {
+ { AppIcon::ICON_DOCUMENT, "document" },
+ { AppIcon::ICON_APPLICATION, "application" },
+ { AppIcon::ICON_SHARED_DOCUMENT, "documentShared" },
+};
+
+// Converts |url_string| to |result|. Always returns true to be used
+// for JSONValueConverter::RegisterCustomField method.
+// TODO(mukai): make it return false in case of invalid |url_string|.
+bool GetGURLFromString(const base::StringPiece& url_string, GURL* result) {
+ *result = GURL(url_string.as_string());
+ return true;
+}
+
+// Converts boolean string values like "true" into bool.
+bool GetBoolFromString(const base::StringPiece& value, bool* result) {
+ *result = (value == "true");
+ return true;
+}
+
+bool SortBySize(const InstalledApp::IconList::value_type& a,
+ const InstalledApp::IconList::value_type& b) {
+ return a.first < b.first;
+}
+
+} // namespace
+
+////////////////////////////////////////////////////////////////////////////////
+// Author implementation
+
+Author::Author() {
+}
+
+// static
+void Author::RegisterJSONConverter(
+ base::JSONValueConverter<Author>* converter) {
+ converter->RegisterStringField(kNameField, &Author::name_);
+ converter->RegisterStringField(kEmailField, &Author::email_);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// Link implementation
+
+Link::Link() : type_(Link::LINK_UNKNOWN) {
+}
+
+Link::~Link() {
+}
+
+// static
+bool Link::GetAppID(const base::StringPiece& rel, std::string* app_id) {
+ DCHECK(app_id);
+ // Fast return path if the link clearly isn't an OPEN_WITH link.
+ if (rel.size() < kOpenWithPrefixSize) {
+ app_id->clear();
+ return true;
+ }
+
+ const std::string kOpenWithPrefixStr(kOpenWithPrefix);
+ if (StartsWithASCII(rel.as_string(), kOpenWithPrefixStr, false)) {
+ *app_id = rel.as_string().substr(kOpenWithPrefixStr.size());
+ return true;
+ }
+
+ app_id->clear();
+ return true;
+}
+
+// static.
+bool Link::GetLinkType(const base::StringPiece& rel, Link::LinkType* type) {
+ DCHECK(type);
+ for (size_t i = 0; i < arraysize(kLinkTypeMap); i++) {
+ if (rel == kLinkTypeMap[i].rel) {
+ *type = kLinkTypeMap[i].type;
+ return true;
+ }
+ }
+
+ // OPEN_WITH links have extra information at the end of the rel that is unique
+ // for each one, so we can't just check the usual map. This check is slightly
+ // redundant to provide a quick skip if it's obviously not an OPEN_WITH url.
+ if (rel.size() >= kOpenWithPrefixSize &&
+ StartsWithASCII(rel.as_string(), kOpenWithPrefix, false)) {
+ *type = LINK_OPEN_WITH;
+ return true;
+ }
+
+ // Let unknown link types through, just report it; if the link type is needed
+ // in the future, add it into LinkType and kLinkTypeMap.
+ DVLOG(1) << "Ignoring unknown link type for rel " << rel;
+ *type = LINK_UNKNOWN;
+ return true;
+}
+
+// static
+void Link::RegisterJSONConverter(base::JSONValueConverter<Link>* converter) {
+ converter->RegisterCustomField<Link::LinkType>(kRelField,
+ &Link::type_,
+ &Link::GetLinkType);
+ // We have to register kRelField twice because we extract two different pieces
+ // of data from the same rel field.
+ converter->RegisterCustomField<std::string>(kRelField,
+ &Link::app_id_,
+ &Link::GetAppID);
+ converter->RegisterCustomField(kHrefField, &Link::href_, &GetGURLFromString);
+ converter->RegisterStringField(kTitleField, &Link::title_);
+ converter->RegisterStringField(kTypeField, &Link::mime_type_);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// ResourceLink implementation
+
+ResourceLink::ResourceLink() : type_(ResourceLink::FEED_LINK_UNKNOWN) {
+}
+
+// static.
+bool ResourceLink::GetFeedLinkType(
+ const base::StringPiece& rel, ResourceLink::ResourceLinkType* result) {
+ for (size_t i = 0; i < arraysize(kFeedLinkTypeMap); i++) {
+ if (rel == kFeedLinkTypeMap[i].rel) {
+ *result = kFeedLinkTypeMap[i].type;
+ return true;
+ }
+ }
+ DVLOG(1) << "Unknown feed link type for rel " << rel;
+ return false;
+}
+
+// static
+void ResourceLink::RegisterJSONConverter(
+ base::JSONValueConverter<ResourceLink>* converter) {
+ converter->RegisterCustomField<ResourceLink::ResourceLinkType>(
+ kRelField, &ResourceLink::type_, &ResourceLink::GetFeedLinkType);
+ converter->RegisterCustomField(
+ kHrefField, &ResourceLink::href_, &GetGURLFromString);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// Category implementation
+
+Category::Category() : type_(CATEGORY_UNKNOWN) {
+}
+
+// Converts category.scheme into CategoryType enum.
+bool Category::GetCategoryTypeFromScheme(
+ const base::StringPiece& scheme, Category::CategoryType* result) {
+ for (size_t i = 0; i < arraysize(kCategoryTypeMap); i++) {
+ if (scheme == kCategoryTypeMap[i].scheme) {
+ *result = kCategoryTypeMap[i].type;
+ return true;
+ }
+ }
+ DVLOG(1) << "Unknown feed link type for scheme " << scheme;
+ return false;
+}
+
+// static
+void Category::RegisterJSONConverter(
+ base::JSONValueConverter<Category>* converter) {
+ converter->RegisterStringField(kLabelField, &Category::label_);
+ converter->RegisterCustomField<Category::CategoryType>(
+ kSchemeField, &Category::type_, &Category::GetCategoryTypeFromScheme);
+ converter->RegisterStringField(kTermField, &Category::term_);
+}
+
+const Link* CommonMetadata::GetLinkByType(Link::LinkType type) const {
+ for (size_t i = 0; i < links_.size(); ++i) {
+ if (links_[i]->type() == type)
+ return links_[i];
+ }
+ return NULL;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// Content implementation
+
+Content::Content() {
+}
+
+// static
+void Content::RegisterJSONConverter(
+ base::JSONValueConverter<Content>* converter) {
+ converter->RegisterCustomField(kSrcField, &Content::url_, &GetGURLFromString);
+ converter->RegisterStringField(kTypeField, &Content::mime_type_);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// AppIcon implementation
+
+AppIcon::AppIcon() : category_(AppIcon::ICON_UNKNOWN), icon_side_length_(0) {
+}
+
+AppIcon::~AppIcon() {
+}
+
+// static
+void AppIcon::RegisterJSONConverter(
+ base::JSONValueConverter<AppIcon>* converter) {
+ converter->RegisterCustomField<AppIcon::IconCategory>(
+ kInstalledAppIconCategoryField,
+ &AppIcon::category_,
+ &AppIcon::GetIconCategory);
+ converter->RegisterCustomField<int>(kInstalledAppIconSizeField,
+ &AppIcon::icon_side_length_,
+ base::StringToInt);
+ converter->RegisterRepeatedMessage(kLinkField, &AppIcon::links_);
+}
+
+GURL AppIcon::GetIconURL() const {
+ for (size_t i = 0; i < links_.size(); ++i) {
+ if (links_[i]->type() == Link::LINK_ICON)
+ return links_[i]->href();
+ }
+ return GURL();
+}
+
+// static
+bool AppIcon::GetIconCategory(const base::StringPiece& category,
+ AppIcon::IconCategory* result) {
+ for (size_t i = 0; i < arraysize(kAppIconCategoryMap); i++) {
+ if (category == kAppIconCategoryMap[i].category_name) {
+ *result = kAppIconCategoryMap[i].category;
+ return true;
+ }
+ }
+ DVLOG(1) << "Unknown icon category " << category;
+ return false;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// CommonMetadata implementation
+
+CommonMetadata::CommonMetadata() {
+}
+
+CommonMetadata::~CommonMetadata() {
+}
+
+// static
+template<typename CommonMetadataDescendant>
+void CommonMetadata::RegisterJSONConverter(
+ base::JSONValueConverter<CommonMetadataDescendant>* converter) {
+ converter->RegisterStringField(kETagField, &CommonMetadata::etag_);
+ converter->template RegisterRepeatedMessage<Author>(
+ kAuthorField, &CommonMetadata::authors_);
+ converter->template RegisterRepeatedMessage<Link>(
+ kLinkField, &CommonMetadata::links_);
+ converter->template RegisterRepeatedMessage<Category>(
+ kCategoryField, &CommonMetadata::categories_);
+ converter->template RegisterCustomField<base::Time>(
+ kUpdatedField, &CommonMetadata::updated_time_, &util::GetTimeFromString);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// ResourceEntry implementation
+
+ResourceEntry::ResourceEntry()
+ : kind_(ENTRY_KIND_UNKNOWN),
+ file_size_(0),
+ deleted_(false),
+ removed_(false),
+ changestamp_(0),
+ image_width_(-1),
+ image_height_(-1),
+ image_rotation_(-1) {
+}
+
+ResourceEntry::~ResourceEntry() {
+}
+
+bool ResourceEntry::HasFieldPresent(const base::Value* value,
+ bool* result) {
+ *result = (value != NULL);
+ return true;
+}
+
+bool ResourceEntry::ParseChangestamp(const base::Value* value,
+ int64* result) {
+ DCHECK(result);
+ if (!value) {
+ *result = 0;
+ return true;
+ }
+
+ std::string string_value;
+ if (value->GetAsString(&string_value) &&
+ base::StringToInt64(string_value, result))
+ return true;
+
+ return false;
+}
+
+// static
+void ResourceEntry::RegisterJSONConverter(
+ base::JSONValueConverter<ResourceEntry>* converter) {
+ // Inherit the parent registrations.
+ CommonMetadata::RegisterJSONConverter(converter);
+ converter->RegisterStringField(
+ kResourceIdField, &ResourceEntry::resource_id_);
+ converter->RegisterStringField(kIDField, &ResourceEntry::id_);
+ converter->RegisterStringField(kTitleTField, &ResourceEntry::title_);
+ converter->RegisterCustomField<base::Time>(
+ kPublishedField, &ResourceEntry::published_time_,
+ &util::GetTimeFromString);
+ converter->RegisterCustomField<base::Time>(
+ kLastViewedField, &ResourceEntry::last_viewed_time_,
+ &util::GetTimeFromString);
+ converter->RegisterRepeatedMessage(
+ kFeedLinkField, &ResourceEntry::resource_links_);
+ converter->RegisterNestedField(kContentField, &ResourceEntry::content_);
+
+ // File properties. If the resource type is not a normal file, then
+ // that's no problem because those feed must not have these fields
+ // themselves, which does not report errors.
+ converter->RegisterStringField(kFileNameField, &ResourceEntry::filename_);
+ converter->RegisterStringField(kMD5Field, &ResourceEntry::file_md5_);
+ converter->RegisterCustomField<int64>(
+ kSizeField, &ResourceEntry::file_size_, &base::StringToInt64);
+ converter->RegisterStringField(
+ kSuggestedFileNameField, &ResourceEntry::suggested_filename_);
+ // Deleted are treated as 'trashed' items on web client side. Removed files
+ // are gone for good. We treat both cases as 'deleted' for this client.
+ converter->RegisterCustomValueField<bool>(
+ kDeletedField, &ResourceEntry::deleted_, &ResourceEntry::HasFieldPresent);
+ converter->RegisterCustomValueField<bool>(
+ kRemovedField, &ResourceEntry::removed_, &ResourceEntry::HasFieldPresent);
+ converter->RegisterCustomValueField<int64>(
+ kChangestampField, &ResourceEntry::changestamp_,
+ &ResourceEntry::ParseChangestamp);
+ // ImageMediaMetadata fields are not supported by WAPI.
+}
+
+std::string ResourceEntry::GetHostedDocumentExtension() const {
+ for (size_t i = 0; i < arraysize(kEntryKindMap); i++) {
+ if (kEntryKindMap[i].kind == kind_) {
+ if (kEntryKindMap[i].extension)
+ return std::string(kEntryKindMap[i].extension);
+ else
+ return std::string();
+ }
+ }
+ return std::string();
+}
+
+// static
+int ResourceEntry::ClassifyEntryKindByFileExtension(
+ const base::FilePath& file_path) {
+#if defined(OS_WIN)
+ std::string file_extension = WideToUTF8(file_path.Extension());
+#else
+ std::string file_extension = file_path.Extension();
+#endif
+ for (size_t i = 0; i < arraysize(kEntryKindMap); ++i) {
+ const char* document_extension = kEntryKindMap[i].extension;
+ if (document_extension && file_extension == document_extension)
+ return ClassifyEntryKind(kEntryKindMap[i].kind);
+ }
+ return 0;
+}
+
+// static
+DriveEntryKind ResourceEntry::GetEntryKindFromTerm(
+ const std::string& term) {
+ if (!StartsWithASCII(term, kTermPrefix, false)) {
+ DVLOG(1) << "Unexpected term prefix term " << term;
+ return ENTRY_KIND_UNKNOWN;
+ }
+
+ std::string type = term.substr(strlen(kTermPrefix));
+ for (size_t i = 0; i < arraysize(kEntryKindMap); i++) {
+ if (type == kEntryKindMap[i].entry)
+ return kEntryKindMap[i].kind;
+ }
+ DVLOG(1) << "Unknown entry type for term " << term << ", type " << type;
+ return ENTRY_KIND_UNKNOWN;
+}
+
+// static
+int ResourceEntry::ClassifyEntryKind(DriveEntryKind kind) {
+ int classes = 0;
+
+ // All DriveEntryKind members are listed here, so the compiler catches if a
+ // newly added member is missing here.
+ switch (kind) {
+ case ENTRY_KIND_UNKNOWN:
+ // Special entries.
+ case ENTRY_KIND_ITEM:
+ case ENTRY_KIND_SITE:
+ break;
+
+ // Hosted Google document.
+ case ENTRY_KIND_DOCUMENT:
+ case ENTRY_KIND_SPREADSHEET:
+ case ENTRY_KIND_PRESENTATION:
+ case ENTRY_KIND_DRAWING:
+ case ENTRY_KIND_TABLE:
+ case ENTRY_KIND_FORM:
+ classes = KIND_OF_GOOGLE_DOCUMENT | KIND_OF_HOSTED_DOCUMENT;
+ break;
+
+ // Hosted external application document.
+ case ENTRY_KIND_EXTERNAL_APP:
+ classes = KIND_OF_EXTERNAL_DOCUMENT | KIND_OF_HOSTED_DOCUMENT;
+ break;
+
+ // Folders, collections.
+ case ENTRY_KIND_FOLDER:
+ classes = KIND_OF_FOLDER;
+ break;
+
+ // Regular files.
+ case ENTRY_KIND_FILE:
+ case ENTRY_KIND_PDF:
+ classes = KIND_OF_FILE;
+ break;
+
+ case ENTRY_KIND_MAX_VALUE:
+ NOTREACHED();
+ }
+
+ return classes;
+}
+
+void ResourceEntry::FillRemainingFields() {
+ // Set |kind_| and |labels_| based on the |categories_| in the class.
+ // JSONValueConverter does not have the ability to catch an element in a list
+ // based on a predicate. Thus we need to iterate over |categories_| and
+ // find the elements to set these fields as a post-process.
+ for (size_t i = 0; i < categories_.size(); ++i) {
+ const Category* category = categories_[i];
+ if (category->type() == Category::CATEGORY_KIND)
+ kind_ = GetEntryKindFromTerm(category->term());
+ else if (category->type() == Category::CATEGORY_LABEL)
+ labels_.push_back(category->label());
+ }
+}
+
+// static
+scoped_ptr<ResourceEntry> ResourceEntry::ExtractAndParse(
+ const base::Value& value) {
+ const base::DictionaryValue* as_dict = NULL;
+ const base::DictionaryValue* entry_dict = NULL;
+ if (value.GetAsDictionary(&as_dict) &&
+ as_dict->GetDictionary(kEntryField, &entry_dict)) {
+ return ResourceEntry::CreateFrom(*entry_dict);
+ }
+ return scoped_ptr<ResourceEntry>();
+}
+
+// static
+scoped_ptr<ResourceEntry> ResourceEntry::CreateFrom(const base::Value& value) {
+ base::JSONValueConverter<ResourceEntry> converter;
+ scoped_ptr<ResourceEntry> entry(new ResourceEntry());
+ if (!converter.Convert(value, entry.get())) {
+ DVLOG(1) << "Invalid resource entry!";
+ return scoped_ptr<ResourceEntry>();
+ }
+
+ entry->FillRemainingFields();
+ return entry.Pass();
+}
+
+// static
+std::string ResourceEntry::GetEntryNodeName() {
+ return kEntryNode;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// ResourceList implementation
+
+ResourceList::ResourceList()
+ : start_index_(0),
+ items_per_page_(0),
+ largest_changestamp_(0) {
+}
+
+ResourceList::~ResourceList() {
+}
+
+// static
+void ResourceList::RegisterJSONConverter(
+ base::JSONValueConverter<ResourceList>* converter) {
+ // inheritance
+ CommonMetadata::RegisterJSONConverter(converter);
+ // TODO(zelidrag): Once we figure out where these will be used, we should
+ // check for valid start_index_ and items_per_page_ values.
+ converter->RegisterCustomField<int>(
+ kStartIndexField, &ResourceList::start_index_, &base::StringToInt);
+ converter->RegisterCustomField<int>(
+ kItemsPerPageField, &ResourceList::items_per_page_, &base::StringToInt);
+ converter->RegisterStringField(kTitleTField, &ResourceList::title_);
+ converter->RegisterRepeatedMessage(kEntryField, &ResourceList::entries_);
+ converter->RegisterCustomField<int64>(
+ kLargestChangestampField, &ResourceList::largest_changestamp_,
+ &base::StringToInt64);
+}
+
+bool ResourceList::Parse(const base::Value& value) {
+ base::JSONValueConverter<ResourceList> converter;
+ if (!converter.Convert(value, this)) {
+ DVLOG(1) << "Invalid resource list!";
+ return false;
+ }
+
+ ScopedVector<ResourceEntry>::iterator iter = entries_.begin();
+ while (iter != entries_.end()) {
+ ResourceEntry* entry = (*iter);
+ entry->FillRemainingFields();
+ ++iter;
+ }
+ return true;
+}
+
+// static
+scoped_ptr<ResourceList> ResourceList::ExtractAndParse(
+ const base::Value& value) {
+ const base::DictionaryValue* as_dict = NULL;
+ const base::DictionaryValue* feed_dict = NULL;
+ if (value.GetAsDictionary(&as_dict) &&
+ as_dict->GetDictionary(kFeedField, &feed_dict)) {
+ return ResourceList::CreateFrom(*feed_dict);
+ }
+ return scoped_ptr<ResourceList>();
+}
+
+// static
+scoped_ptr<ResourceList> ResourceList::CreateFrom(const base::Value& value) {
+ scoped_ptr<ResourceList> feed(new ResourceList());
+ if (!feed->Parse(value)) {
+ DVLOG(1) << "Invalid resource list!";
+ return scoped_ptr<ResourceList>();
+ }
+
+ return feed.Pass();
+}
+
+bool ResourceList::GetNextFeedURL(GURL* url) const {
+ DCHECK(url);
+ for (size_t i = 0; i < links_.size(); ++i) {
+ if (links_[i]->type() == Link::LINK_NEXT) {
+ *url = links_[i]->href();
+ return true;
+ }
+ }
+ return false;
+}
+
+void ResourceList::ReleaseEntries(std::vector<ResourceEntry*>* entries) {
+ entries_.release(entries);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// InstalledApp implementation
+
+InstalledApp::InstalledApp() : supports_create_(false) {
+}
+
+InstalledApp::~InstalledApp() {
+}
+
+InstalledApp::IconList InstalledApp::GetIconsForCategory(
+ AppIcon::IconCategory category) const {
+ IconList result;
+
+ for (ScopedVector<AppIcon>::const_iterator icon_iter = app_icons_.begin();
+ icon_iter != app_icons_.end(); ++icon_iter) {
+ if ((*icon_iter)->category() != category)
+ continue;
+ GURL icon_url = (*icon_iter)->GetIconURL();
+ if (icon_url.is_empty())
+ continue;
+ result.push_back(std::make_pair((*icon_iter)->icon_side_length(),
+ icon_url));
+ }
+
+ // Return a sorted list, smallest to largest.
+ std::sort(result.begin(), result.end(), SortBySize);
+ return result;
+}
+
+GURL InstalledApp::GetProductUrl() const {
+ for (ScopedVector<Link>::const_iterator it = links_.begin();
+ it != links_.end(); ++it) {
+ const Link* link = *it;
+ if (link->type() == Link::LINK_PRODUCT)
+ return link->href();
+ }
+ return GURL();
+}
+
+// static
+bool InstalledApp::GetValueString(const base::Value* value,
+ std::string* result) {
+ const base::DictionaryValue* dict = NULL;
+ if (!value->GetAsDictionary(&dict))
+ return false;
+
+ if (!dict->GetString(kTField, result))
+ return false;
+
+ return true;
+}
+
+// static
+void InstalledApp::RegisterJSONConverter(
+ base::JSONValueConverter<InstalledApp>* converter) {
+ converter->RegisterRepeatedMessage(kInstalledAppIconField,
+ &InstalledApp::app_icons_);
+ converter->RegisterStringField(kInstalledAppIdField,
+ &InstalledApp::app_id_);
+ converter->RegisterStringField(kInstalledAppNameField,
+ &InstalledApp::app_name_);
+ converter->RegisterStringField(kInstalledAppObjectTypeField,
+ &InstalledApp::object_type_);
+ converter->RegisterCustomField<bool>(kInstalledAppSupportsCreateField,
+ &InstalledApp::supports_create_,
+ &GetBoolFromString);
+ converter->RegisterRepeatedCustomValue(kInstalledAppPrimaryMimeTypeField,
+ &InstalledApp::primary_mimetypes_,
+ &GetValueString);
+ converter->RegisterRepeatedCustomValue(kInstalledAppSecondaryMimeTypeField,
+ &InstalledApp::secondary_mimetypes_,
+ &GetValueString);
+ converter->RegisterRepeatedCustomValue(kInstalledAppPrimaryFileExtensionField,
+ &InstalledApp::primary_extensions_,
+ &GetValueString);
+ converter->RegisterRepeatedCustomValue(
+ kInstalledAppSecondaryFileExtensionField,
+ &InstalledApp::secondary_extensions_,
+ &GetValueString);
+ converter->RegisterRepeatedMessage(kLinkField, &InstalledApp::links_);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// AccountMetadata implementation
+
+AccountMetadata::AccountMetadata()
+ : quota_bytes_total_(0),
+ quota_bytes_used_(0),
+ largest_changestamp_(0) {
+}
+
+AccountMetadata::~AccountMetadata() {
+}
+
+// static
+void AccountMetadata::RegisterJSONConverter(
+ base::JSONValueConverter<AccountMetadata>* converter) {
+ converter->RegisterCustomField<int64>(
+ kQuotaBytesTotalField,
+ &AccountMetadata::quota_bytes_total_,
+ &base::StringToInt64);
+ converter->RegisterCustomField<int64>(
+ kQuotaBytesUsedField,
+ &AccountMetadata::quota_bytes_used_,
+ &base::StringToInt64);
+ converter->RegisterCustomField<int64>(
+ kLargestChangestampField,
+ &AccountMetadata::largest_changestamp_,
+ &base::StringToInt64);
+ converter->RegisterRepeatedMessage(kInstalledAppField,
+ &AccountMetadata::installed_apps_);
+}
+
+// static
+scoped_ptr<AccountMetadata> AccountMetadata::CreateFrom(
+ const base::Value& value) {
+ scoped_ptr<AccountMetadata> metadata(new AccountMetadata());
+ const base::DictionaryValue* dictionary = NULL;
+ const base::Value* entry = NULL;
+ if (!value.GetAsDictionary(&dictionary) ||
+ !dictionary->Get(kEntryField, &entry) ||
+ !metadata->Parse(*entry)) {
+ LOG(ERROR) << "Unable to create: Invalid account metadata feed!";
+ return scoped_ptr<AccountMetadata>();
+ }
+
+ return metadata.Pass();
+}
+
+bool AccountMetadata::Parse(const base::Value& value) {
+ base::JSONValueConverter<AccountMetadata> converter;
+ if (!converter.Convert(value, this)) {
+ LOG(ERROR) << "Unable to parse: Invalid account metadata feed!";
+ return false;
+ }
+ return true;
+}
+
+} // namespace google_apis
diff --git a/chromium/google_apis/drive/gdata_wapi_parser.h b/chromium/google_apis/drive/gdata_wapi_parser.h
new file mode 100644
index 00000000000..0c89c11b09e
--- /dev/null
+++ b/chromium/google_apis/drive/gdata_wapi_parser.h
@@ -0,0 +1,866 @@
+// Copyright (c) 2012 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef GOOGLE_APIS_DRIVE_GDATA_WAPI_PARSER_H_
+#define GOOGLE_APIS_DRIVE_GDATA_WAPI_PARSER_H_
+
+#include <string>
+#include <utility>
+#include <vector>
+
+#include "base/compiler_specific.h"
+#include "base/memory/scoped_ptr.h"
+#include "base/memory/scoped_vector.h"
+#include "base/strings/string_piece.h"
+#include "base/time/time.h"
+#include "google_apis/drive/drive_entry_kinds.h"
+#include "url/gurl.h"
+
+namespace base {
+class FilePath;
+class DictionaryValue;
+class Value;
+
+template <class StructType>
+class JSONValueConverter;
+
+namespace internal {
+template <class NestedType>
+class RepeatedMessageConverter;
+} // namespace internal
+
+} // namespace base
+
+// Defines data elements of Google Documents API as described in
+// http://code.google.com/apis/documents/.
+namespace google_apis {
+
+// Defines link (URL) of an entity (document, file, feed...). Each entity could
+// have more than one link representing it.
+class Link {
+ public:
+ enum LinkType {
+ LINK_UNKNOWN,
+ LINK_SELF,
+ LINK_NEXT,
+ LINK_PARENT,
+ LINK_ALTERNATE,
+ LINK_EDIT,
+ LINK_EDIT_MEDIA,
+ LINK_ALT_EDIT_MEDIA,
+ LINK_ALT_POST,
+ LINK_FEED,
+ LINK_POST,
+ LINK_BATCH,
+ LINK_RESUMABLE_EDIT_MEDIA,
+ LINK_RESUMABLE_CREATE_MEDIA,
+ LINK_TABLES_FEED,
+ LINK_WORKSHEET_FEED,
+ LINK_THUMBNAIL,
+ LINK_EMBED,
+ LINK_PRODUCT,
+ LINK_ICON,
+ LINK_OPEN_WITH,
+ LINK_SHARE,
+ };
+ Link();
+ ~Link();
+
+ // Registers the mapping between JSON field names and the members in
+ // this class.
+ static void RegisterJSONConverter(base::JSONValueConverter<Link>* converter);
+
+ // Type of the link.
+ LinkType type() const { return type_; }
+
+ // URL of the link.
+ const GURL& href() const { return href_; }
+
+ // Title of the link.
+ const std::string& title() const { return title_; }
+
+ // For OPEN_WITH links, this contains the application ID. For all other link
+ // types, it is the empty string.
+ const std::string& app_id() const { return app_id_; }
+
+ // Link MIME type.
+ const std::string& mime_type() const { return mime_type_; }
+
+ void set_type(LinkType type) { type_ = type; }
+ void set_href(const GURL& href) { href_ = href; }
+ void set_title(const std::string& title) { title_ = title; }
+ void set_app_id(const std::string& app_id) { app_id_ = app_id; }
+ void set_mime_type(const std::string& mime_type) { mime_type_ = mime_type; }
+
+ private:
+ friend class ResourceEntry;
+ // Converts value of link.rel into LinkType. Outputs to |type| and returns
+ // true when |rel| has a valid value. Otherwise does nothing and returns
+ // false.
+ static bool GetLinkType(const base::StringPiece& rel, LinkType* type);
+
+ // Converts value of link.rel to application ID, if there is one embedded in
+ // the link.rel field. Outputs to |app_id| and returns true when |rel| has a
+ // valid value. Otherwise does nothing and returns false.
+ static bool GetAppID(const base::StringPiece& rel, std::string* app_id);
+
+ LinkType type_;
+ GURL href_;
+ std::string title_;
+ std::string app_id_;
+ std::string mime_type_;
+
+ DISALLOW_COPY_AND_ASSIGN(Link);
+};
+
+// Feed links define links (URLs) to special list of entries (i.e. list of
+// previous document revisions).
+class ResourceLink {
+ public:
+ enum ResourceLinkType {
+ FEED_LINK_UNKNOWN,
+ FEED_LINK_ACL,
+ FEED_LINK_REVISIONS,
+ };
+ ResourceLink();
+
+ // Registers the mapping between JSON field names and the members in
+ // this class.
+ static void RegisterJSONConverter(
+ base::JSONValueConverter<ResourceLink>* converter);
+
+ // MIME type of the feed.
+ ResourceLinkType type() const { return type_; }
+
+ // URL of the feed.
+ const GURL& href() const { return href_; }
+
+ void set_type(ResourceLinkType type) { type_ = type; }
+ void set_href(const GURL& href) { href_ = href; }
+
+ private:
+ friend class ResourceEntry;
+ // Converts value of gd$feedLink.rel into ResourceLinkType enum.
+ // Outputs to |result| and returns true when |rel| has a valid
+ // value. Otherwise does nothing and returns false.
+ static bool GetFeedLinkType(
+ const base::StringPiece& rel, ResourceLinkType* result);
+
+ ResourceLinkType type_;
+ GURL href_;
+
+ DISALLOW_COPY_AND_ASSIGN(ResourceLink);
+};
+
+// Author represents an author of an entity.
+class Author {
+ public:
+ Author();
+
+ // Registers the mapping between JSON field names and the members in
+ // this class.
+ static void RegisterJSONConverter(
+ base::JSONValueConverter<Author>* converter);
+
+ // Getters.
+ const std::string& name() const { return name_; }
+ const std::string& email() const { return email_; }
+
+ void set_name(const std::string& name) { name_ = name; }
+ void set_email(const std::string& email) { email_ = email; }
+
+ private:
+ friend class ResourceEntry;
+
+ std::string name_;
+ std::string email_;
+
+ DISALLOW_COPY_AND_ASSIGN(Author);
+};
+
+// Entry category.
+class Category {
+ public:
+ enum CategoryType {
+ CATEGORY_UNKNOWN,
+ CATEGORY_ITEM,
+ CATEGORY_KIND,
+ CATEGORY_LABEL,
+ };
+
+ Category();
+
+ // Registers the mapping between JSON field names and the members in
+ // this class.
+ static void RegisterJSONConverter(
+ base::JSONValueConverter<Category>* converter);
+
+ // Category label.
+ const std::string& label() const { return label_; }
+
+ // Category type.
+ CategoryType type() const { return type_; }
+
+ // Category term.
+ const std::string& term() const { return term_; }
+
+ void set_label(const std::string& label) { label_ = label; }
+ void set_type(CategoryType type) { type_ = type; }
+ void set_term(const std::string& term) { term_ = term; }
+
+ private:
+ friend class ResourceEntry;
+ // Converts category scheme into CategoryType enum. For example,
+ // http://schemas.google.com/g/2005#kind => Category::CATEGORY_KIND
+ // Returns false and does not change |result| when |scheme| has an
+ // unrecognizable value.
+ static bool GetCategoryTypeFromScheme(
+ const base::StringPiece& scheme, CategoryType* result);
+
+ std::string label_;
+ CategoryType type_;
+ std::string term_;
+
+ DISALLOW_COPY_AND_ASSIGN(Category);
+};
+
+// Content details of a resource: mime-type, url, and so on.
+class Content {
+ public:
+ Content();
+
+ // Registers the mapping between JSON field names and the members in
+ // this class.
+ static void RegisterJSONConverter(
+ base::JSONValueConverter<Content>* converter);
+
+ // The URL to download the file content.
+ // Note that the url can expire, so we'll fetch the latest resource
+ // entry before starting a download to get the download URL.
+ const GURL& url() const { return url_; }
+ const std::string& mime_type() const { return mime_type_; }
+
+ void set_url(const GURL& url) { url_ = url; }
+ void set_mime_type(const std::string& mime_type) { mime_type_ = mime_type; }
+
+ private:
+ friend class ResourceEntry;
+
+ GURL url_;
+ std::string mime_type_;
+};
+
+// This stores a representation of an application icon as registered with the
+// installed applications section of the account metadata feed. There can be
+// multiple icons registered for each application, differing in size, category
+// and MIME type.
+class AppIcon {
+ public:
+ enum IconCategory {
+ ICON_UNKNOWN, // Uninitialized state
+ ICON_DOCUMENT, // Document icon for various MIME types
+ ICON_APPLICATION, // Application icon for various MIME types
+ ICON_SHARED_DOCUMENT, // Icon for documents that are shared from other
+ // users.
+ };
+
+ AppIcon();
+ ~AppIcon();
+
+ // Registers the mapping between JSON field names and the members in
+ // this class.
+ static void RegisterJSONConverter(
+ base::JSONValueConverter<AppIcon>* converter);
+
+ // Category of the icon.
+ IconCategory category() const { return category_; }
+
+ // Size in pixels of one side of the icon (icons are always square).
+ int icon_side_length() const { return icon_side_length_; }
+
+ // Get a list of links available for this AppIcon.
+ const ScopedVector<Link>& links() const { return links_; }
+
+ // Get the icon URL from the internal list of links. Returns the first
+ // icon URL found in the list.
+ GURL GetIconURL() const;
+
+ void set_category(IconCategory category) { category_ = category; }
+ void set_icon_side_length(int icon_side_length) {
+ icon_side_length_ = icon_side_length;
+ }
+ void set_links(ScopedVector<Link> links) { links_ = links.Pass(); }
+
+ private:
+ // Extracts the icon category from the given string. Returns false and does
+ // not change |result| when |scheme| has an unrecognizable value.
+ static bool GetIconCategory(const base::StringPiece& category,
+ IconCategory* result);
+
+ IconCategory category_;
+ int icon_side_length_;
+ ScopedVector<Link> links_;
+
+ DISALLOW_COPY_AND_ASSIGN(AppIcon);
+};
+
+// Base class for feed entries. This class defines fields commonly used by
+// various feeds.
+class CommonMetadata {
+ public:
+ CommonMetadata();
+ virtual ~CommonMetadata();
+
+ // Returns a link of a given |type| for this entry. If not found, it returns
+ // NULL.
+ const Link* GetLinkByType(Link::LinkType type) const;
+
+ // Entry update time.
+ base::Time updated_time() const { return updated_time_; }
+
+ // Entry ETag.
+ const std::string& etag() const { return etag_; }
+
+ // List of entry authors.
+ const ScopedVector<Author>& authors() const { return authors_; }
+
+ // List of entry links.
+ const ScopedVector<Link>& links() const { return links_; }
+ ScopedVector<Link>* mutable_links() { return &links_; }
+
+ // List of entry categories.
+ const ScopedVector<Category>& categories() const { return categories_; }
+
+ void set_etag(const std::string& etag) { etag_ = etag; }
+ void set_authors(ScopedVector<Author> authors) {
+ authors_ = authors.Pass();
+ }
+ void set_links(ScopedVector<Link> links) {
+ links_ = links.Pass();
+ }
+ void set_categories(ScopedVector<Category> categories) {
+ categories_ = categories.Pass();
+ }
+ void set_updated_time(const base::Time& updated_time) {
+ updated_time_ = updated_time;
+ }
+
+ protected:
+ // Registers the mapping between JSON field names and the members in
+ // this class.
+ template<typename CommonMetadataDescendant>
+ static void RegisterJSONConverter(
+ base::JSONValueConverter<CommonMetadataDescendant>* converter);
+
+ std::string etag_;
+ ScopedVector<Author> authors_;
+ ScopedVector<Link> links_;
+ ScopedVector<Category> categories_;
+ base::Time updated_time_;
+
+ DISALLOW_COPY_AND_ASSIGN(CommonMetadata);
+};
+
+// This class represents a resource entry. A resource is a generic term which
+// refers to a file and a directory.
+class ResourceEntry : public CommonMetadata {
+ public:
+ ResourceEntry();
+ virtual ~ResourceEntry();
+
+ // Extracts "entry" dictionary from the JSON value, and parse the contents,
+ // using CreateFrom(). Returns NULL on failure. The input JSON data, coming
+ // from the gdata server, looks like:
+ //
+ // {
+ // "encoding": "UTF-8",
+ // "entry": { ... }, // This function will extract this and parse.
+ // "version": "1.0"
+ // }
+ //
+ // The caller should delete the returned object.
+ static scoped_ptr<ResourceEntry> ExtractAndParse(const base::Value& value);
+
+ // Creates resource entry from parsed JSON Value. You should call
+ // this instead of instantiating JSONValueConverter by yourself
+ // because this method does some post-process for some fields. See
+ // FillRemainingFields comment and implementation for the details.
+ static scoped_ptr<ResourceEntry> CreateFrom(const base::Value& value);
+
+ // Returns name of entry node.
+ static std::string GetEntryNodeName();
+
+ // Registers the mapping between JSON field names and the members in
+ // this class.
+ static void RegisterJSONConverter(
+ base::JSONValueConverter<ResourceEntry>* converter);
+
+ // Sets true to |result| if the field exists.
+ // Always returns true even when the field does not exist.
+ static bool HasFieldPresent(const base::Value* value, bool* result);
+
+ // Parses |value| as int64 and sets it to |result|. If the field does not
+ // exist, sets 0 to |result| as default value.
+ // Returns true if |value| is NULL or it is parsed as int64 successfully.
+ static bool ParseChangestamp(const base::Value* value, int64* result);
+
+ // The resource ID is used to identify a resource, which looks like:
+ // file:d41d8cd98f00b204e9800998ecf8
+ const std::string& resource_id() const { return resource_id_; }
+
+ // This is a URL looks like:
+ // https://docs.google.com/feeds/id/file%3Ad41d8cd98f00b204e9800998ecf8.
+ // The URL is currently not used.
+ const std::string& id() const { return id_; }
+
+ DriveEntryKind kind() const { return kind_; }
+ const std::string& title() const { return title_; }
+ base::Time published_time() const { return published_time_; }
+ base::Time last_viewed_time() const { return last_viewed_time_; }
+ const std::vector<std::string>& labels() const { return labels_; }
+
+ // The URL to download a file content.
+ // Search for 'download_url' in gdata_wapi_requests.h for details.
+ const GURL& download_url() const { return content_.url(); }
+
+ const std::string& content_mime_type() const { return content_.mime_type(); }
+
+ // The resource links contain extra links for revisions and access control,
+ // etc. Note that links() contain more basic links like edit URL,
+ // alternative URL, etc.
+ const ScopedVector<ResourceLink>& resource_links() const {
+ return resource_links_;
+ }
+
+ // File name (exists only for kinds FILE and PDF).
+ const std::string& filename() const { return filename_; }
+
+ // Suggested file name (exists only for kinds FILE and PDF).
+ const std::string& suggested_filename() const { return suggested_filename_; }
+
+ // File content MD5 (exists only for kinds FILE and PDF).
+ const std::string& file_md5() const { return file_md5_; }
+
+ // File size (exists only for kinds FILE and PDF).
+ int64 file_size() const { return file_size_; }
+
+ // True if the file or directory is deleted (applicable to change list only).
+ bool deleted() const { return deleted_ || removed_; }
+
+ // Changestamp (exists only for change query results).
+ // If not exists, defaults to 0.
+ int64 changestamp() const { return changestamp_; }
+
+ // Image width (exists only for images).
+ // If doesn't exist, then equals -1.
+ int64 image_width() const { return image_width_; }
+
+ // Image height (exists only for images).
+ // If doesn't exist, then equals -1.
+ int64 image_height() const { return image_height_; }
+
+ // Image rotation in clockwise degrees (exists only for images).
+ // If doesn't exist, then equals -1.
+ int64 image_rotation() const { return image_rotation_; }
+
+ // Text version of resource entry kind. Returns an empty string for
+ // unknown entry kind.
+ std::string GetEntryKindText() const;
+
+ // Returns preferred file extension for hosted documents. If entry is not
+ // a hosted document, this call returns an empty string.
+ std::string GetHostedDocumentExtension() const;
+
+ // True if resource entry is remotely hosted.
+ bool is_hosted_document() const {
+ return (ClassifyEntryKind(kind_) & KIND_OF_HOSTED_DOCUMENT) > 0;
+ }
+ // True if resource entry hosted by Google Documents.
+ bool is_google_document() const {
+ return (ClassifyEntryKind(kind_) & KIND_OF_GOOGLE_DOCUMENT) > 0;
+ }
+ // True if resource entry is hosted by an external application.
+ bool is_external_document() const {
+ return (ClassifyEntryKind(kind_) & KIND_OF_EXTERNAL_DOCUMENT) > 0;
+ }
+ // True if resource entry is a folder (collection).
+ bool is_folder() const {
+ return (ClassifyEntryKind(kind_) & KIND_OF_FOLDER) > 0;
+ }
+ // True if resource entry is regular file.
+ bool is_file() const {
+ return (ClassifyEntryKind(kind_) & KIND_OF_FILE) > 0;
+ }
+ // True if resource entry can't be mapped to the file system.
+ bool is_special() const {
+ return !is_file() && !is_folder() && !is_hosted_document();
+ }
+
+ // The following constructs are exposed for unit tests.
+
+ // Classes of EntryKind. Used for ClassifyEntryKind().
+ enum EntryKindClass {
+ KIND_OF_NONE = 0,
+ KIND_OF_HOSTED_DOCUMENT = 1,
+ KIND_OF_GOOGLE_DOCUMENT = 1 << 1,
+ KIND_OF_EXTERNAL_DOCUMENT = 1 << 2,
+ KIND_OF_FOLDER = 1 << 3,
+ KIND_OF_FILE = 1 << 4,
+ };
+
+ // Classifies the EntryKind. The returned value is a bitmask of
+ // EntryKindClass. For example, DOCUMENT is classified as
+ // KIND_OF_HOSTED_DOCUMENT and KIND_OF_GOOGLE_DOCUMENT, hence the returned
+ // value is KIND_OF_HOSTED_DOCUMENT | KIND_OF_GOOGLE_DOCUMENT.
+ static int ClassifyEntryKind(DriveEntryKind kind);
+
+ // Classifies the EntryKind by the file extension of specific path. The
+ // returned value is a bitmask of EntryKindClass. See also ClassifyEntryKind.
+ static int ClassifyEntryKindByFileExtension(const base::FilePath& file);
+
+ void set_resource_id(const std::string& resource_id) {
+ resource_id_ = resource_id;
+ }
+ void set_id(const std::string& id) { id_ = id; }
+ void set_kind(DriveEntryKind kind) { kind_ = kind; }
+ void set_title(const std::string& title) { title_ = title; }
+ void set_published_time(const base::Time& published_time) {
+ published_time_ = published_time;
+ }
+ void set_last_viewed_time(const base::Time& last_viewed_time) {
+ last_viewed_time_ = last_viewed_time;
+ }
+ void set_labels(const std::vector<std::string>& labels) {
+ labels_ = labels;
+ }
+ void set_content(const Content& content) {
+ content_ = content;
+ }
+ void set_resource_links(ScopedVector<ResourceLink> resource_links) {
+ resource_links_ = resource_links.Pass();
+ }
+ void set_filename(const std::string& filename) { filename_ = filename; }
+ void set_suggested_filename(const std::string& suggested_filename) {
+ suggested_filename_ = suggested_filename;
+ }
+ void set_file_md5(const std::string& file_md5) { file_md5_ = file_md5; }
+ void set_file_size(int64 file_size) { file_size_ = file_size; }
+ void set_deleted(bool deleted) { deleted_ = deleted; }
+ void set_removed(bool removed) { removed_ = removed; }
+ void set_changestamp(int64 changestamp) { changestamp_ = changestamp; }
+ void set_image_width(int64 image_width) { image_width_ = image_width; }
+ void set_image_height(int64 image_height) { image_height_ = image_height; }
+ void set_image_rotation(int64 image_rotation) {
+ image_rotation_ = image_rotation;
+ }
+
+ // Fills the remaining fields where JSONValueConverter cannot catch.
+ // Currently, sets |kind_| and |labels_| based on the |categories_| in the
+ // class.
+ void FillRemainingFields();
+
+ private:
+ friend class base::internal::RepeatedMessageConverter<ResourceEntry>;
+ friend class ResourceList;
+ friend class ResumeUploadRequest;
+
+ // Converts categories.term into DriveEntryKind enum.
+ static DriveEntryKind GetEntryKindFromTerm(const std::string& term);
+ // Converts |kind| into its text identifier equivalent.
+ static const char* GetEntryKindDescription(DriveEntryKind kind);
+
+ std::string resource_id_;
+ std::string id_;
+ DriveEntryKind kind_;
+ std::string title_;
+ base::Time published_time_;
+ // Last viewed value may be unreliable. See: crbug.com/152628.
+ base::Time last_viewed_time_;
+ std::vector<std::string> labels_;
+ Content content_;
+ ScopedVector<ResourceLink> resource_links_;
+ // Optional fields for files only.
+ std::string filename_;
+ std::string suggested_filename_;
+ std::string file_md5_;
+ int64 file_size_;
+ bool deleted_;
+ bool removed_;
+ int64 changestamp_;
+ int64 image_width_;
+ int64 image_height_;
+ int64 image_rotation_;
+
+ DISALLOW_COPY_AND_ASSIGN(ResourceEntry);
+};
+
+// This class represents a list of resource entries with some extra metadata
+// such as the root upload URL. The feed is paginated and the rest of the
+// feed can be fetched by retrieving the remaining parts of the feed from
+// URLs provided by GetNextFeedURL() method.
+class ResourceList : public CommonMetadata {
+ public:
+ ResourceList();
+ virtual ~ResourceList();
+
+ // Extracts "feed" dictionary from the JSON value, and parse the contents,
+ // using CreateFrom(). Returns NULL on failure. The input JSON data, coming
+ // from the gdata server, looks like:
+ //
+ // {
+ // "encoding": "UTF-8",
+ // "feed": { ... }, // This function will extract this and parse.
+ // "version": "1.0"
+ // }
+ static scoped_ptr<ResourceList> ExtractAndParse(const base::Value& value);
+
+ // Creates feed from parsed JSON Value. You should call this
+ // instead of instantiating JSONValueConverter by yourself because
+ // this method does some post-process for some fields. See
+ // FillRemainingFields comment and implementation in ResourceEntry
+ // class for the details.
+ static scoped_ptr<ResourceList> CreateFrom(const base::Value& value);
+
+ // Registers the mapping between JSON field names and the members in
+ // this class.
+ static void RegisterJSONConverter(
+ base::JSONValueConverter<ResourceList>* converter);
+
+ // Returns true and passes|url| of the next feed if the current entry list
+ // does not completed this feed.
+ bool GetNextFeedURL(GURL* url) const;
+
+ // List of resource entries.
+ const ScopedVector<ResourceEntry>& entries() const { return entries_; }
+ ScopedVector<ResourceEntry>* mutable_entries() { return &entries_; }
+
+ // Releases entries_ into |entries|. This is a transfer of ownership, so the
+ // caller is responsible for deleting the elements of |entries|.
+ void ReleaseEntries(std::vector<ResourceEntry*>* entries);
+
+ // Start index of the resource entry list.
+ int start_index() const { return start_index_; }
+
+ // Number of items per feed of the resource entry list.
+ int items_per_page() const { return items_per_page_; }
+
+ // The largest changestamp. Next time the resource list should be fetched
+ // from this changestamp.
+ int64 largest_changestamp() const { return largest_changestamp_; }
+
+ // Resource entry list title.
+ const std::string& title() { return title_; }
+
+ void set_entries(ScopedVector<ResourceEntry> entries) {
+ entries_ = entries.Pass();
+ }
+ void set_start_index(int start_index) {
+ start_index_ = start_index;
+ }
+ void set_items_per_page(int items_per_page) {
+ items_per_page_ = items_per_page;
+ }
+ void set_title(const std::string& title) {
+ title_ = title;
+ }
+ void set_largest_changestamp(int64 largest_changestamp) {
+ largest_changestamp_ = largest_changestamp;
+ }
+
+ private:
+ // Parses and initializes data members from content of |value|.
+ // Return false if parsing fails.
+ bool Parse(const base::Value& value);
+
+ ScopedVector<ResourceEntry> entries_;
+ int start_index_;
+ int items_per_page_;
+ std::string title_;
+ int64 largest_changestamp_;
+
+ DISALLOW_COPY_AND_ASSIGN(ResourceList);
+};
+
+// Metadata representing installed Google Drive application.
+class InstalledApp {
+ public:
+ typedef std::vector<std::pair<int, GURL> > IconList;
+
+ InstalledApp();
+ virtual ~InstalledApp();
+
+ // WebApp name.
+ const std::string& app_name() const { return app_name_; }
+
+ // Drive app id
+ const std::string& app_id() const { return app_id_; }
+
+ // Object (file) type name that is generated by this WebApp.
+ const std::string& object_type() const { return object_type_; }
+
+ // True if WebApp supports creation of new file instances.
+ bool supports_create() const { return supports_create_; }
+
+ // List of primary mime types supported by this WebApp. Primary status should
+ // trigger this WebApp becoming the default handler of file instances that
+ // have these mime types.
+ const ScopedVector<std::string>& primary_mimetypes() const {
+ return primary_mimetypes_;
+ }
+
+ // List of secondary mime types supported by this WebApp. Secondary status
+ // should make this WebApp show up in "Open with..." pop-up menu of the
+ // default action menu for file with matching mime types.
+ const ScopedVector<std::string>& secondary_mimetypes() const {
+ return secondary_mimetypes_;
+ }
+
+ // List of primary file extensions supported by this WebApp. Primary status
+ // should trigger this WebApp becoming the default handler of file instances
+ // that match these extensions.
+ const ScopedVector<std::string>& primary_extensions() const {
+ return primary_extensions_;
+ }
+
+ // List of secondary file extensions supported by this WebApp. Secondary
+ // status should make this WebApp show up in "Open with..." pop-up menu of the
+ // default action menu for file with matching extensions.
+ const ScopedVector<std::string>& secondary_extensions() const {
+ return secondary_extensions_;
+ }
+
+ // List of entry links.
+ const ScopedVector<Link>& links() const { return links_; }
+
+ // Returns a list of icons associated with this installed application.
+ const ScopedVector<AppIcon>& app_icons() const {
+ return app_icons_;
+ }
+
+ // Convenience function for getting the icon URLs for a particular |category|
+ // of icon. Icons are returned in a sorted list, from smallest to largest.
+ IconList GetIconsForCategory(AppIcon::IconCategory category) const;
+
+ // Retrieves product URL from the link collection.
+ GURL GetProductUrl() const;
+
+ // Registers the mapping between JSON field names and the members in
+ // this class.
+ static void RegisterJSONConverter(
+ base::JSONValueConverter<InstalledApp>* converter);
+
+ void set_app_id(const std::string& app_id) { app_id_ = app_id; }
+ void set_app_name(const std::string& app_name) { app_name_ = app_name; }
+ void set_object_type(const std::string& object_type) {
+ object_type_ = object_type;
+ }
+ void set_supports_create(bool supports_create) {
+ supports_create_ = supports_create;
+ }
+ void set_primary_mimetypes(
+ ScopedVector<std::string> primary_mimetypes) {
+ primary_mimetypes_ = primary_mimetypes.Pass();
+ }
+ void set_secondary_mimetypes(
+ ScopedVector<std::string> secondary_mimetypes) {
+ secondary_mimetypes_ = secondary_mimetypes.Pass();
+ }
+ void set_primary_extensions(
+ ScopedVector<std::string> primary_extensions) {
+ primary_extensions_ = primary_extensions.Pass();
+ }
+ void set_secondary_extensions(
+ ScopedVector<std::string> secondary_extensions) {
+ secondary_extensions_ = secondary_extensions.Pass();
+ }
+ void set_links(ScopedVector<Link> links) {
+ links_ = links.Pass();
+ }
+ void set_app_icons(ScopedVector<AppIcon> app_icons) {
+ app_icons_ = app_icons.Pass();
+ }
+
+ private:
+ // Extracts "$t" value from the dictionary |value| and returns it in |result|.
+ // If the string value can't be found, it returns false.
+ static bool GetValueString(const base::Value* value,
+ std::string* result);
+
+ std::string app_id_;
+ std::string app_name_;
+ std::string object_type_;
+ bool supports_create_;
+ ScopedVector<std::string> primary_mimetypes_;
+ ScopedVector<std::string> secondary_mimetypes_;
+ ScopedVector<std::string> primary_extensions_;
+ ScopedVector<std::string> secondary_extensions_;
+ ScopedVector<Link> links_;
+ ScopedVector<AppIcon> app_icons_;
+};
+
+// Account metadata feed represents the metadata object attached to the user's
+// account.
+class AccountMetadata {
+ public:
+ AccountMetadata();
+ virtual ~AccountMetadata();
+
+ // Creates feed from parsed JSON Value. You should call this
+ // instead of instantiating JSONValueConverter by yourself because
+ // this method does some post-process for some fields. See
+ // FillRemainingFields comment and implementation in ResourceEntry
+ // class for the details.
+ static scoped_ptr<AccountMetadata> CreateFrom(const base::Value& value);
+
+ int64 quota_bytes_total() const {
+ return quota_bytes_total_;
+ }
+
+ int64 quota_bytes_used() const {
+ return quota_bytes_used_;
+ }
+
+ int64 largest_changestamp() const {
+ return largest_changestamp_;
+ }
+
+ const ScopedVector<InstalledApp>& installed_apps() const {
+ return installed_apps_;
+ }
+
+ void set_quota_bytes_total(int64 quota_bytes_total) {
+ quota_bytes_total_ = quota_bytes_total;
+ }
+ void set_quota_bytes_used(int64 quota_bytes_used) {
+ quota_bytes_used_ = quota_bytes_used;
+ }
+ void set_largest_changestamp(int64 largest_changestamp) {
+ largest_changestamp_ = largest_changestamp;
+ }
+ void set_installed_apps(ScopedVector<InstalledApp> installed_apps) {
+ installed_apps_ = installed_apps.Pass();
+ }
+
+ // Registers the mapping between JSON field names and the members in
+ // this class.
+ static void RegisterJSONConverter(
+ base::JSONValueConverter<AccountMetadata>* converter);
+
+ private:
+ // Parses and initializes data members from content of |value|.
+ // Return false if parsing fails.
+ bool Parse(const base::Value& value);
+
+ int64 quota_bytes_total_;
+ int64 quota_bytes_used_;
+ int64 largest_changestamp_;
+ ScopedVector<InstalledApp> installed_apps_;
+
+ DISALLOW_COPY_AND_ASSIGN(AccountMetadata);
+};
+
+
+} // namespace google_apis
+
+#endif // GOOGLE_APIS_DRIVE_GDATA_WAPI_PARSER_H_
diff --git a/chromium/google_apis/drive/gdata_wapi_parser_unittest.cc b/chromium/google_apis/drive/gdata_wapi_parser_unittest.cc
new file mode 100644
index 00000000000..58728d129ba
--- /dev/null
+++ b/chromium/google_apis/drive/gdata_wapi_parser_unittest.cc
@@ -0,0 +1,389 @@
+// Copyright (c) 2012 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "google_apis/drive/gdata_wapi_parser.h"
+
+#include <string>
+
+#include "base/files/file_path.h"
+#include "base/json/json_file_value_serializer.h"
+#include "base/logging.h"
+#include "base/time/time.h"
+#include "base/values.h"
+#include "google_apis/drive/test_util.h"
+#include "google_apis/drive/time_util.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+namespace google_apis {
+
+// TODO(nhiroki): Move json files to out of 'chromeos' directory
+// (http://crbug.com/149788).
+// Test document feed parsing.
+TEST(GDataWAPIParserTest, ResourceListJsonParser) {
+ std::string error;
+ scoped_ptr<base::Value> document =
+ test_util::LoadJSONFile("gdata/basic_feed.json");
+ ASSERT_TRUE(document.get());
+ ASSERT_EQ(base::Value::TYPE_DICTIONARY, document->GetType());
+ scoped_ptr<ResourceList> feed(ResourceList::ExtractAndParse(*document));
+ ASSERT_TRUE(feed.get());
+
+ base::Time update_time;
+ ASSERT_TRUE(util::GetTimeFromString("2011-12-14T01:03:21.151Z",
+ &update_time));
+
+ EXPECT_EQ(1, feed->start_index());
+ EXPECT_EQ(1000, feed->items_per_page());
+ EXPECT_EQ(update_time, feed->updated_time());
+
+ // Check authors.
+ ASSERT_EQ(1U, feed->authors().size());
+ EXPECT_EQ("tester", feed->authors()[0]->name());
+ EXPECT_EQ("tester@testing.com", feed->authors()[0]->email());
+
+ // Check links.
+ ASSERT_EQ(6U, feed->links().size());
+ const Link* self_link = feed->GetLinkByType(Link::LINK_SELF);
+ ASSERT_TRUE(self_link);
+ EXPECT_EQ("https://self_link/", self_link->href().spec());
+ EXPECT_EQ("application/atom+xml", self_link->mime_type());
+
+ const Link* resumable_link =
+ feed->GetLinkByType(Link::LINK_RESUMABLE_CREATE_MEDIA);
+ ASSERT_TRUE(resumable_link);
+ EXPECT_EQ("https://resumable_create_media_link/",
+ resumable_link->href().spec());
+ EXPECT_EQ("application/atom+xml", resumable_link->mime_type());
+
+ // Check entries.
+ ASSERT_EQ(4U, feed->entries().size());
+
+ // Check a folder entry.
+ const ResourceEntry* folder_entry = feed->entries()[0];
+ ASSERT_TRUE(folder_entry);
+ EXPECT_EQ(ENTRY_KIND_FOLDER, folder_entry->kind());
+ EXPECT_EQ("\"HhMOFgcNHSt7ImBr\"", folder_entry->etag());
+ EXPECT_EQ("folder:sub_sub_directory_folder_id", folder_entry->resource_id());
+ EXPECT_EQ("https://1_folder_id", folder_entry->id());
+ EXPECT_EQ("Entry 1 Title", folder_entry->title());
+ base::Time entry1_update_time;
+ base::Time entry1_publish_time;
+ ASSERT_TRUE(util::GetTimeFromString("2011-04-01T18:34:08.234Z",
+ &entry1_update_time));
+ ASSERT_TRUE(util::GetTimeFromString("2010-11-07T05:03:54.719Z",
+ &entry1_publish_time));
+ EXPECT_EQ(entry1_update_time, folder_entry->updated_time());
+ EXPECT_EQ(entry1_publish_time, folder_entry->published_time());
+
+ ASSERT_EQ(1U, folder_entry->authors().size());
+ EXPECT_EQ("entry_tester", folder_entry->authors()[0]->name());
+ EXPECT_EQ("entry_tester@testing.com", folder_entry->authors()[0]->email());
+ EXPECT_EQ("https://1_folder_content_url/",
+ folder_entry->download_url().spec());
+ EXPECT_EQ("application/atom+xml;type=feed",
+ folder_entry->content_mime_type());
+
+ ASSERT_EQ(1U, folder_entry->resource_links().size());
+ const ResourceLink* feed_link = folder_entry->resource_links()[0];
+ ASSERT_TRUE(feed_link);
+ ASSERT_EQ(ResourceLink::FEED_LINK_ACL, feed_link->type());
+
+ const Link* entry1_alternate_link =
+ folder_entry->GetLinkByType(Link::LINK_ALTERNATE);
+ ASSERT_TRUE(entry1_alternate_link);
+ EXPECT_EQ("https://1_folder_alternate_link/",
+ entry1_alternate_link->href().spec());
+ EXPECT_EQ("text/html", entry1_alternate_link->mime_type());
+
+ const Link* entry1_edit_link = folder_entry->GetLinkByType(Link::LINK_EDIT);
+ ASSERT_TRUE(entry1_edit_link);
+ EXPECT_EQ("https://1_edit_link/", entry1_edit_link->href().spec());
+ EXPECT_EQ("application/atom+xml", entry1_edit_link->mime_type());
+
+ // Check a file entry.
+ const ResourceEntry* file_entry = feed->entries()[1];
+ ASSERT_TRUE(file_entry);
+ EXPECT_EQ(ENTRY_KIND_FILE, file_entry->kind());
+ EXPECT_EQ("filename.m4a", file_entry->filename());
+ EXPECT_EQ("sugg_file_name.m4a", file_entry->suggested_filename());
+ EXPECT_EQ("3b4382ebefec6e743578c76bbd0575ce", file_entry->file_md5());
+ EXPECT_EQ(892721, file_entry->file_size());
+ const Link* file_parent_link = file_entry->GetLinkByType(Link::LINK_PARENT);
+ ASSERT_TRUE(file_parent_link);
+ EXPECT_EQ("https://file_link_parent/", file_parent_link->href().spec());
+ EXPECT_EQ("application/atom+xml", file_parent_link->mime_type());
+ EXPECT_EQ("Medical", file_parent_link->title());
+ const Link* file_open_with_link =
+ file_entry->GetLinkByType(Link::LINK_OPEN_WITH);
+ ASSERT_TRUE(file_open_with_link);
+ EXPECT_EQ("https://xml_file_entry_open_with_link/",
+ file_open_with_link->href().spec());
+ EXPECT_EQ("application/atom+xml", file_open_with_link->mime_type());
+ EXPECT_EQ("the_app_id", file_open_with_link->app_id());
+ EXPECT_EQ(654321, file_entry->changestamp());
+
+ const Link* file_unknown_link = file_entry->GetLinkByType(Link::LINK_UNKNOWN);
+ ASSERT_TRUE(file_unknown_link);
+ EXPECT_EQ("https://xml_file_fake_entry_open_with_link/",
+ file_unknown_link->href().spec());
+ EXPECT_EQ("application/atom+xml", file_unknown_link->mime_type());
+ EXPECT_EQ("", file_unknown_link->app_id());
+
+ // Check a file entry.
+ const ResourceEntry* resource_entry = feed->entries()[2];
+ ASSERT_TRUE(resource_entry);
+ EXPECT_EQ(ENTRY_KIND_DOCUMENT, resource_entry->kind());
+ EXPECT_TRUE(resource_entry->is_hosted_document());
+ EXPECT_TRUE(resource_entry->is_google_document());
+ EXPECT_FALSE(resource_entry->is_external_document());
+
+ // Check an external document entry.
+ const ResourceEntry* app_entry = feed->entries()[3];
+ ASSERT_TRUE(app_entry);
+ EXPECT_EQ(ENTRY_KIND_EXTERNAL_APP, app_entry->kind());
+ EXPECT_TRUE(app_entry->is_hosted_document());
+ EXPECT_TRUE(app_entry->is_external_document());
+ EXPECT_FALSE(app_entry->is_google_document());
+}
+
+
+// Test document feed parsing.
+TEST(GDataWAPIParserTest, ResourceEntryJsonParser) {
+ std::string error;
+ scoped_ptr<base::Value> document =
+ test_util::LoadJSONFile("gdata/file_entry.json");
+ ASSERT_TRUE(document.get());
+ ASSERT_EQ(base::Value::TYPE_DICTIONARY, document->GetType());
+ scoped_ptr<ResourceEntry> entry(ResourceEntry::ExtractAndParse(*document));
+ ASSERT_TRUE(entry.get());
+
+ EXPECT_EQ(ENTRY_KIND_FILE, entry->kind());
+ EXPECT_EQ("\"HhMOFgxXHit7ImBr\"", entry->etag());
+ EXPECT_EQ("file:2_file_resource_id", entry->resource_id());
+ EXPECT_EQ("2_file_id", entry->id());
+ EXPECT_EQ("File 1.mp3", entry->title());
+ base::Time entry1_update_time;
+ base::Time entry1_publish_time;
+ ASSERT_TRUE(util::GetTimeFromString("2011-12-14T00:40:47.330Z",
+ &entry1_update_time));
+ ASSERT_TRUE(util::GetTimeFromString("2011-12-13T00:40:47.330Z",
+ &entry1_publish_time));
+ EXPECT_EQ(entry1_update_time, entry->updated_time());
+ EXPECT_EQ(entry1_publish_time, entry->published_time());
+
+ EXPECT_EQ(1U, entry->authors().size());
+ EXPECT_EQ("tester", entry->authors()[0]->name());
+ EXPECT_EQ("tester@testing.com", entry->authors()[0]->email());
+ EXPECT_EQ("https://file_content_url/",
+ entry->download_url().spec());
+ EXPECT_EQ("audio/mpeg",
+ entry->content_mime_type());
+
+ // Check feed links.
+ ASSERT_EQ(1U, entry->resource_links().size());
+ const ResourceLink* feed_link_1 = entry->resource_links()[0];
+ ASSERT_TRUE(feed_link_1);
+ EXPECT_EQ(ResourceLink::FEED_LINK_REVISIONS, feed_link_1->type());
+
+ // Check links.
+ ASSERT_EQ(8U, entry->links().size());
+ const Link* entry1_alternate_link =
+ entry->GetLinkByType(Link::LINK_ALTERNATE);
+ ASSERT_TRUE(entry1_alternate_link);
+ EXPECT_EQ("https://file_link_alternate/",
+ entry1_alternate_link->href().spec());
+ EXPECT_EQ("text/html", entry1_alternate_link->mime_type());
+
+ const Link* entry1_edit_link = entry->GetLinkByType(Link::LINK_EDIT_MEDIA);
+ ASSERT_TRUE(entry1_edit_link);
+ EXPECT_EQ("https://file_edit_media/",
+ entry1_edit_link->href().spec());
+ EXPECT_EQ("audio/mpeg", entry1_edit_link->mime_type());
+
+ const Link* entry1_self_link = entry->GetLinkByType(Link::LINK_SELF);
+ ASSERT_TRUE(entry1_self_link);
+ EXPECT_EQ("https://file1_link_self/file%3A2_file_resource_id",
+ entry1_self_link->href().spec());
+ EXPECT_EQ("application/atom+xml", entry1_self_link->mime_type());
+ EXPECT_EQ("", entry1_self_link->app_id());
+
+ const Link* entry1_open_with_link =
+ entry->GetLinkByType(Link::LINK_OPEN_WITH);
+ ASSERT_TRUE(entry1_open_with_link);
+ EXPECT_EQ("https://entry1_open_with_link/",
+ entry1_open_with_link->href().spec());
+ EXPECT_EQ("application/atom+xml", entry1_open_with_link->mime_type());
+ EXPECT_EQ("the_app_id", entry1_open_with_link->app_id());
+
+ const Link* entry1_unknown_link = entry->GetLinkByType(Link::LINK_UNKNOWN);
+ ASSERT_TRUE(entry1_unknown_link);
+ EXPECT_EQ("https://entry1_fake_entry_open_with_link/",
+ entry1_unknown_link->href().spec());
+ EXPECT_EQ("application/atom+xml", entry1_unknown_link->mime_type());
+ EXPECT_EQ("", entry1_unknown_link->app_id());
+
+ // Check a file properties.
+ EXPECT_EQ(ENTRY_KIND_FILE, entry->kind());
+ EXPECT_EQ("File 1.mp3", entry->filename());
+ EXPECT_EQ("File 1.mp3", entry->suggested_filename());
+ EXPECT_EQ("3b4382ebefec6e743578c76bbd0575ce", entry->file_md5());
+ EXPECT_EQ(892721, entry->file_size());
+
+ // WAPI doesn't provide image metadata, but these fields are available
+ // since this class can wrap data received from Drive API (via a converter).
+ EXPECT_EQ(-1, entry->image_width());
+ EXPECT_EQ(-1, entry->image_height());
+ EXPECT_EQ(-1, entry->image_rotation());
+}
+
+TEST(GDataWAPIParserTest, AccountMetadataParser) {
+ scoped_ptr<base::Value> document =
+ test_util::LoadJSONFile("gdata/account_metadata.json");
+ ASSERT_TRUE(document.get());
+ base::DictionaryValue* document_dict = NULL;
+ base::DictionaryValue* entry_value = NULL;
+ ASSERT_TRUE(document->GetAsDictionary(&document_dict));
+ ASSERT_TRUE(document_dict->GetDictionary(std::string("entry"), &entry_value));
+ ASSERT_TRUE(entry_value);
+
+ scoped_ptr<AccountMetadata> metadata(
+ AccountMetadata::CreateFrom(*document));
+ ASSERT_TRUE(metadata.get());
+ EXPECT_EQ(GG_LONGLONG(6789012345), metadata->quota_bytes_used());
+ EXPECT_EQ(GG_LONGLONG(9876543210), metadata->quota_bytes_total());
+ EXPECT_EQ(654321, metadata->largest_changestamp());
+ EXPECT_EQ(2U, metadata->installed_apps().size());
+ const InstalledApp* first_app = metadata->installed_apps()[0];
+ const InstalledApp* second_app = metadata->installed_apps()[1];
+
+ ASSERT_TRUE(first_app);
+ EXPECT_EQ("Drive App 1", first_app->app_name());
+ EXPECT_EQ("Drive App Object 1", first_app->object_type());
+ EXPECT_TRUE(first_app->supports_create());
+ EXPECT_EQ("https://chrome.google.com/webstore/detail/abcdefabcdef",
+ first_app->GetProductUrl().spec());
+
+ ASSERT_EQ(2U, first_app->primary_mimetypes().size());
+ EXPECT_EQ("application/test_type_1",
+ *first_app->primary_mimetypes()[0]);
+ EXPECT_EQ("application/vnd.google-apps.drive-sdk.11111111",
+ *first_app->primary_mimetypes()[1]);
+
+ ASSERT_EQ(1U, first_app->secondary_mimetypes().size());
+ EXPECT_EQ("image/jpeg", *first_app->secondary_mimetypes()[0]);
+
+ ASSERT_EQ(2U, first_app->primary_extensions().size());
+ EXPECT_EQ("ext_1", *first_app->primary_extensions()[0]);
+ EXPECT_EQ("ext_2", *first_app->primary_extensions()[1]);
+
+ ASSERT_EQ(1U, first_app->secondary_extensions().size());
+ EXPECT_EQ("ext_3", *first_app->secondary_extensions()[0]);
+
+ ASSERT_EQ(1U, first_app->app_icons().size());
+ EXPECT_EQ(AppIcon::ICON_DOCUMENT, first_app->app_icons()[0]->category());
+ EXPECT_EQ(16, first_app->app_icons()[0]->icon_side_length());
+ GURL icon_url = first_app->app_icons()[0]->GetIconURL();
+ EXPECT_EQ("https://www.google.com/images/srpr/logo3w.png", icon_url.spec());
+ InstalledApp::IconList icons =
+ first_app->GetIconsForCategory(AppIcon::ICON_DOCUMENT);
+ EXPECT_EQ("https://www.google.com/images/srpr/logo3w.png",
+ icons[0].second.spec());
+ icons = first_app->GetIconsForCategory(AppIcon::ICON_SHARED_DOCUMENT);
+ EXPECT_TRUE(icons.empty());
+
+ ASSERT_TRUE(second_app);
+ EXPECT_EQ("Drive App 2", second_app->app_name());
+ EXPECT_EQ("Drive App Object 2", second_app->object_type());
+ EXPECT_EQ("https://chrome.google.com/webstore/detail/deadbeefdeadbeef",
+ second_app->GetProductUrl().spec());
+ EXPECT_FALSE(second_app->supports_create());
+ EXPECT_EQ(2U, second_app->primary_mimetypes().size());
+ EXPECT_EQ(0U, second_app->secondary_mimetypes().size());
+ EXPECT_EQ(1U, second_app->primary_extensions().size());
+ EXPECT_EQ(0U, second_app->secondary_extensions().size());
+}
+
+TEST(GDataWAPIParserTest, ClassifyEntryKindByFileExtension) {
+ EXPECT_EQ(
+ ResourceEntry::KIND_OF_GOOGLE_DOCUMENT |
+ ResourceEntry::KIND_OF_HOSTED_DOCUMENT,
+ ResourceEntry::ClassifyEntryKindByFileExtension(
+ base::FilePath(FILE_PATH_LITERAL("Test.gdoc"))));
+ EXPECT_EQ(
+ ResourceEntry::KIND_OF_GOOGLE_DOCUMENT |
+ ResourceEntry::KIND_OF_HOSTED_DOCUMENT,
+ ResourceEntry::ClassifyEntryKindByFileExtension(
+ base::FilePath(FILE_PATH_LITERAL("Test.gsheet"))));
+ EXPECT_EQ(
+ ResourceEntry::KIND_OF_GOOGLE_DOCUMENT |
+ ResourceEntry::KIND_OF_HOSTED_DOCUMENT,
+ ResourceEntry::ClassifyEntryKindByFileExtension(
+ base::FilePath(FILE_PATH_LITERAL("Test.gslides"))));
+ EXPECT_EQ(
+ ResourceEntry::KIND_OF_GOOGLE_DOCUMENT |
+ ResourceEntry::KIND_OF_HOSTED_DOCUMENT,
+ ResourceEntry::ClassifyEntryKindByFileExtension(
+ base::FilePath(FILE_PATH_LITERAL("Test.gdraw"))));
+ EXPECT_EQ(
+ ResourceEntry::KIND_OF_GOOGLE_DOCUMENT |
+ ResourceEntry::KIND_OF_HOSTED_DOCUMENT,
+ ResourceEntry::ClassifyEntryKindByFileExtension(
+ base::FilePath(FILE_PATH_LITERAL("Test.gtable"))));
+ EXPECT_EQ(
+ ResourceEntry::KIND_OF_EXTERNAL_DOCUMENT |
+ ResourceEntry::KIND_OF_HOSTED_DOCUMENT,
+ ResourceEntry::ClassifyEntryKindByFileExtension(
+ base::FilePath(FILE_PATH_LITERAL("Test.glink"))));
+ EXPECT_EQ(
+ ResourceEntry::KIND_OF_NONE,
+ ResourceEntry::ClassifyEntryKindByFileExtension(
+ base::FilePath(FILE_PATH_LITERAL("Test.tar.gz"))));
+ EXPECT_EQ(
+ ResourceEntry::KIND_OF_NONE,
+ ResourceEntry::ClassifyEntryKindByFileExtension(
+ base::FilePath(FILE_PATH_LITERAL("Test.txt"))));
+ EXPECT_EQ(
+ ResourceEntry::KIND_OF_NONE,
+ ResourceEntry::ClassifyEntryKindByFileExtension(
+ base::FilePath(FILE_PATH_LITERAL("Test"))));
+ EXPECT_EQ(
+ ResourceEntry::KIND_OF_NONE,
+ ResourceEntry::ClassifyEntryKindByFileExtension(
+ base::FilePath()));
+}
+
+TEST(GDataWAPIParserTest, ResourceEntryClassifyEntryKind) {
+ EXPECT_EQ(ResourceEntry::KIND_OF_NONE,
+ ResourceEntry::ClassifyEntryKind(ENTRY_KIND_UNKNOWN));
+ EXPECT_EQ(ResourceEntry::KIND_OF_NONE,
+ ResourceEntry::ClassifyEntryKind(ENTRY_KIND_ITEM));
+ EXPECT_EQ(ResourceEntry::KIND_OF_NONE,
+ ResourceEntry::ClassifyEntryKind(ENTRY_KIND_SITE));
+ EXPECT_EQ(ResourceEntry::KIND_OF_GOOGLE_DOCUMENT |
+ ResourceEntry::KIND_OF_HOSTED_DOCUMENT,
+ ResourceEntry::ClassifyEntryKind(ENTRY_KIND_DOCUMENT));
+ EXPECT_EQ(ResourceEntry::KIND_OF_GOOGLE_DOCUMENT |
+ ResourceEntry::KIND_OF_HOSTED_DOCUMENT,
+ ResourceEntry::ClassifyEntryKind(ENTRY_KIND_SPREADSHEET));
+ EXPECT_EQ(ResourceEntry::KIND_OF_GOOGLE_DOCUMENT |
+ ResourceEntry::KIND_OF_HOSTED_DOCUMENT,
+ ResourceEntry::ClassifyEntryKind(ENTRY_KIND_PRESENTATION));
+ EXPECT_EQ(ResourceEntry::KIND_OF_GOOGLE_DOCUMENT |
+ ResourceEntry::KIND_OF_HOSTED_DOCUMENT,
+ ResourceEntry::ClassifyEntryKind(ENTRY_KIND_DRAWING));
+ EXPECT_EQ(ResourceEntry::KIND_OF_GOOGLE_DOCUMENT |
+ ResourceEntry::KIND_OF_HOSTED_DOCUMENT,
+ ResourceEntry::ClassifyEntryKind(ENTRY_KIND_TABLE));
+ EXPECT_EQ(ResourceEntry::KIND_OF_EXTERNAL_DOCUMENT |
+ ResourceEntry::KIND_OF_HOSTED_DOCUMENT,
+ ResourceEntry::ClassifyEntryKind(ENTRY_KIND_EXTERNAL_APP));
+ EXPECT_EQ(ResourceEntry::KIND_OF_FOLDER,
+ ResourceEntry::ClassifyEntryKind(ENTRY_KIND_FOLDER));
+ EXPECT_EQ(ResourceEntry::KIND_OF_FILE,
+ ResourceEntry::ClassifyEntryKind(ENTRY_KIND_FILE));
+ EXPECT_EQ(ResourceEntry::KIND_OF_FILE,
+ ResourceEntry::ClassifyEntryKind(ENTRY_KIND_PDF));
+}
+
+} // namespace google_apis
diff --git a/chromium/google_apis/drive/gdata_wapi_requests.cc b/chromium/google_apis/drive/gdata_wapi_requests.cc
new file mode 100644
index 00000000000..1ae8a95ef08
--- /dev/null
+++ b/chromium/google_apis/drive/gdata_wapi_requests.cc
@@ -0,0 +1,680 @@
+// Copyright (c) 2012 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "google_apis/drive/gdata_wapi_requests.h"
+
+#include "base/location.h"
+#include "base/sequenced_task_runner.h"
+#include "base/task_runner_util.h"
+#include "base/values.h"
+#include "google_apis/drive/gdata_wapi_parser.h"
+#include "google_apis/drive/gdata_wapi_url_generator.h"
+#include "google_apis/drive/request_sender.h"
+#include "google_apis/drive/request_util.h"
+#include "third_party/libxml/chromium/libxml_utils.h"
+
+using net::URLFetcher;
+
+namespace google_apis {
+
+namespace {
+
+// Parses the JSON value to ResourceList.
+scoped_ptr<ResourceList> ParseResourceListOnBlockingPool(
+ scoped_ptr<base::Value> value) {
+ DCHECK(value);
+
+ return ResourceList::ExtractAndParse(*value);
+}
+
+// Runs |callback| with |error| and |resource_list|, but replace the error code
+// with GDATA_PARSE_ERROR, if there was a parsing error.
+void DidParseResourceListOnBlockingPool(
+ const GetResourceListCallback& callback,
+ GDataErrorCode error,
+ scoped_ptr<ResourceList> resource_list) {
+ DCHECK(!callback.is_null());
+
+ // resource_list being NULL indicates there was a parsing error.
+ if (!resource_list)
+ error = GDATA_PARSE_ERROR;
+
+ callback.Run(error, resource_list.Pass());
+}
+
+// Parses the JSON value to ResourceList on the blocking pool and runs
+// |callback| on the UI thread once parsing is done.
+void ParseResourceListAndRun(
+ scoped_refptr<base::TaskRunner> blocking_task_runner,
+ const GetResourceListCallback& callback,
+ GDataErrorCode error,
+ scoped_ptr<base::Value> value) {
+ DCHECK(!callback.is_null());
+
+ if (!value) {
+ callback.Run(error, scoped_ptr<ResourceList>());
+ return;
+ }
+
+ base::PostTaskAndReplyWithResult(
+ blocking_task_runner,
+ FROM_HERE,
+ base::Bind(&ParseResourceListOnBlockingPool, base::Passed(&value)),
+ base::Bind(&DidParseResourceListOnBlockingPool, callback, error));
+}
+
+// Parses the JSON value to AccountMetadata and runs |callback| on the UI
+// thread once parsing is done.
+void ParseAccounetMetadataAndRun(const GetAccountMetadataCallback& callback,
+ GDataErrorCode error,
+ scoped_ptr<base::Value> value) {
+ DCHECK(!callback.is_null());
+
+ if (!value) {
+ callback.Run(error, scoped_ptr<AccountMetadata>());
+ return;
+ }
+
+ // Parsing AccountMetadata is cheap enough to do on UI thread.
+ scoped_ptr<AccountMetadata> entry =
+ google_apis::AccountMetadata::CreateFrom(*value);
+ if (!entry) {
+ callback.Run(GDATA_PARSE_ERROR, scoped_ptr<AccountMetadata>());
+ return;
+ }
+
+ callback.Run(error, entry.Pass());
+}
+
+// Parses the |value| to ResourceEntry with error handling.
+// This is designed to be used for ResumeUploadRequest and
+// GetUploadStatusRequest.
+scoped_ptr<ResourceEntry> ParseResourceEntry(scoped_ptr<base::Value> value) {
+ scoped_ptr<ResourceEntry> entry;
+ if (value.get()) {
+ entry = ResourceEntry::ExtractAndParse(*value);
+
+ // Note: |value| may be NULL, in particular if the callback is for a
+ // failure.
+ if (!entry.get())
+ LOG(WARNING) << "Invalid entry received on upload.";
+ }
+
+ return entry.Pass();
+}
+
+// Extracts the open link url from the JSON Feed. Used by AuthorizeApp().
+void ParseOpenLinkAndRun(const std::string& app_id,
+ const AuthorizeAppCallback& callback,
+ GDataErrorCode error,
+ scoped_ptr<base::Value> value) {
+ DCHECK(!callback.is_null());
+
+ if (!value) {
+ callback.Run(error, GURL());
+ return;
+ }
+
+ // Parsing ResourceEntry is cheap enough to do on UI thread.
+ scoped_ptr<ResourceEntry> resource_entry = ParseResourceEntry(value.Pass());
+ if (!resource_entry) {
+ callback.Run(GDATA_PARSE_ERROR, GURL());
+ return;
+ }
+
+ // Look for the link to open the file with the app with |app_id|.
+ const ScopedVector<Link>& resource_links = resource_entry->links();
+ GURL open_link;
+ for (size_t i = 0; i < resource_links.size(); ++i) {
+ const Link& link = *resource_links[i];
+ if (link.type() == Link::LINK_OPEN_WITH && link.app_id() == app_id) {
+ open_link = link.href();
+ break;
+ }
+ }
+
+ if (open_link.is_empty())
+ error = GDATA_OTHER_ERROR;
+
+ callback.Run(error, open_link);
+}
+
+} // namespace
+
+//============================ GetResourceListRequest ========================
+
+GetResourceListRequest::GetResourceListRequest(
+ RequestSender* sender,
+ const GDataWapiUrlGenerator& url_generator,
+ const GURL& override_url,
+ int64 start_changestamp,
+ const std::string& search_string,
+ const std::string& directory_resource_id,
+ const GetResourceListCallback& callback)
+ : GetDataRequest(
+ sender,
+ base::Bind(&ParseResourceListAndRun,
+ make_scoped_refptr(sender->blocking_task_runner()),
+ callback)),
+ url_generator_(url_generator),
+ override_url_(override_url),
+ start_changestamp_(start_changestamp),
+ search_string_(search_string),
+ directory_resource_id_(directory_resource_id) {
+ DCHECK(!callback.is_null());
+}
+
+GetResourceListRequest::~GetResourceListRequest() {}
+
+GURL GetResourceListRequest::GetURL() const {
+ return url_generator_.GenerateResourceListUrl(override_url_,
+ start_changestamp_,
+ search_string_,
+ directory_resource_id_);
+}
+
+//============================ SearchByTitleRequest ==========================
+
+SearchByTitleRequest::SearchByTitleRequest(
+ RequestSender* sender,
+ const GDataWapiUrlGenerator& url_generator,
+ const std::string& title,
+ const std::string& directory_resource_id,
+ const GetResourceListCallback& callback)
+ : GetDataRequest(
+ sender,
+ base::Bind(&ParseResourceListAndRun,
+ make_scoped_refptr(sender->blocking_task_runner()),
+ callback)),
+ url_generator_(url_generator),
+ title_(title),
+ directory_resource_id_(directory_resource_id) {
+ DCHECK(!callback.is_null());
+}
+
+SearchByTitleRequest::~SearchByTitleRequest() {}
+
+GURL SearchByTitleRequest::GetURL() const {
+ return url_generator_.GenerateSearchByTitleUrl(
+ title_, directory_resource_id_);
+}
+
+//============================ GetResourceEntryRequest =======================
+
+GetResourceEntryRequest::GetResourceEntryRequest(
+ RequestSender* sender,
+ const GDataWapiUrlGenerator& url_generator,
+ const std::string& resource_id,
+ const GURL& embed_origin,
+ const GetDataCallback& callback)
+ : GetDataRequest(sender, callback),
+ url_generator_(url_generator),
+ resource_id_(resource_id),
+ embed_origin_(embed_origin) {
+ DCHECK(!callback.is_null());
+}
+
+GetResourceEntryRequest::~GetResourceEntryRequest() {}
+
+GURL GetResourceEntryRequest::GetURL() const {
+ return url_generator_.GenerateEditUrlWithEmbedOrigin(
+ resource_id_, embed_origin_);
+}
+
+//========================= GetAccountMetadataRequest ========================
+
+GetAccountMetadataRequest::GetAccountMetadataRequest(
+ RequestSender* sender,
+ const GDataWapiUrlGenerator& url_generator,
+ const GetAccountMetadataCallback& callback,
+ bool include_installed_apps)
+ : GetDataRequest(sender,
+ base::Bind(&ParseAccounetMetadataAndRun, callback)),
+ url_generator_(url_generator),
+ include_installed_apps_(include_installed_apps) {
+ DCHECK(!callback.is_null());
+}
+
+GetAccountMetadataRequest::~GetAccountMetadataRequest() {}
+
+GURL GetAccountMetadataRequest::GetURL() const {
+ return url_generator_.GenerateAccountMetadataUrl(include_installed_apps_);
+}
+
+//=========================== DeleteResourceRequest ==========================
+
+DeleteResourceRequest::DeleteResourceRequest(
+ RequestSender* sender,
+ const GDataWapiUrlGenerator& url_generator,
+ const EntryActionCallback& callback,
+ const std::string& resource_id,
+ const std::string& etag)
+ : EntryActionRequest(sender, callback),
+ url_generator_(url_generator),
+ resource_id_(resource_id),
+ etag_(etag) {
+ DCHECK(!callback.is_null());
+}
+
+DeleteResourceRequest::~DeleteResourceRequest() {}
+
+GURL DeleteResourceRequest::GetURL() const {
+ return url_generator_.GenerateEditUrl(resource_id_);
+}
+
+URLFetcher::RequestType DeleteResourceRequest::GetRequestType() const {
+ return URLFetcher::DELETE_REQUEST;
+}
+
+std::vector<std::string>
+DeleteResourceRequest::GetExtraRequestHeaders() const {
+ std::vector<std::string> headers;
+ headers.push_back(util::GenerateIfMatchHeader(etag_));
+ return headers;
+}
+
+//========================== CreateDirectoryRequest ==========================
+
+CreateDirectoryRequest::CreateDirectoryRequest(
+ RequestSender* sender,
+ const GDataWapiUrlGenerator& url_generator,
+ const GetDataCallback& callback,
+ const std::string& parent_resource_id,
+ const std::string& directory_title)
+ : GetDataRequest(sender, callback),
+ url_generator_(url_generator),
+ parent_resource_id_(parent_resource_id),
+ directory_title_(directory_title) {
+ DCHECK(!callback.is_null());
+}
+
+CreateDirectoryRequest::~CreateDirectoryRequest() {}
+
+GURL CreateDirectoryRequest::GetURL() const {
+ return url_generator_.GenerateContentUrl(parent_resource_id_);
+}
+
+URLFetcher::RequestType
+CreateDirectoryRequest::GetRequestType() const {
+ return URLFetcher::POST;
+}
+
+bool CreateDirectoryRequest::GetContentData(std::string* upload_content_type,
+ std::string* upload_content) {
+ upload_content_type->assign("application/atom+xml");
+ XmlWriter xml_writer;
+ xml_writer.StartWriting();
+ xml_writer.StartElement("entry");
+ xml_writer.AddAttribute("xmlns", "http://www.w3.org/2005/Atom");
+
+ xml_writer.StartElement("category");
+ xml_writer.AddAttribute("scheme",
+ "http://schemas.google.com/g/2005#kind");
+ xml_writer.AddAttribute("term",
+ "http://schemas.google.com/docs/2007#folder");
+ xml_writer.EndElement(); // Ends "category" element.
+
+ xml_writer.WriteElement("title", directory_title_);
+
+ xml_writer.EndElement(); // Ends "entry" element.
+ xml_writer.StopWriting();
+ upload_content->assign(xml_writer.GetWrittenString());
+ DVLOG(1) << "CreateDirectory data: " << *upload_content_type << ", ["
+ << *upload_content << "]";
+ return true;
+}
+
+//=========================== RenameResourceRequest ==========================
+
+RenameResourceRequest::RenameResourceRequest(
+ RequestSender* sender,
+ const GDataWapiUrlGenerator& url_generator,
+ const EntryActionCallback& callback,
+ const std::string& resource_id,
+ const std::string& new_title)
+ : EntryActionRequest(sender, callback),
+ url_generator_(url_generator),
+ resource_id_(resource_id),
+ new_title_(new_title) {
+ DCHECK(!callback.is_null());
+}
+
+RenameResourceRequest::~RenameResourceRequest() {}
+
+URLFetcher::RequestType RenameResourceRequest::GetRequestType() const {
+ return URLFetcher::PUT;
+}
+
+std::vector<std::string>
+RenameResourceRequest::GetExtraRequestHeaders() const {
+ std::vector<std::string> headers;
+ headers.push_back(util::kIfMatchAllHeader);
+ return headers;
+}
+
+GURL RenameResourceRequest::GetURL() const {
+ return url_generator_.GenerateEditUrl(resource_id_);
+}
+
+bool RenameResourceRequest::GetContentData(std::string* upload_content_type,
+ std::string* upload_content) {
+ upload_content_type->assign("application/atom+xml");
+ XmlWriter xml_writer;
+ xml_writer.StartWriting();
+ xml_writer.StartElement("entry");
+ xml_writer.AddAttribute("xmlns", "http://www.w3.org/2005/Atom");
+
+ xml_writer.WriteElement("title", new_title_);
+
+ xml_writer.EndElement(); // Ends "entry" element.
+ xml_writer.StopWriting();
+ upload_content->assign(xml_writer.GetWrittenString());
+ DVLOG(1) << "RenameResourceRequest data: " << *upload_content_type << ", ["
+ << *upload_content << "]";
+ return true;
+}
+
+//=========================== AuthorizeAppRequest ==========================
+
+AuthorizeAppRequest::AuthorizeAppRequest(
+ RequestSender* sender,
+ const GDataWapiUrlGenerator& url_generator,
+ const AuthorizeAppCallback& callback,
+ const std::string& resource_id,
+ const std::string& app_id)
+ : GetDataRequest(sender,
+ base::Bind(&ParseOpenLinkAndRun, app_id, callback)),
+ url_generator_(url_generator),
+ resource_id_(resource_id),
+ app_id_(app_id) {
+ DCHECK(!callback.is_null());
+}
+
+AuthorizeAppRequest::~AuthorizeAppRequest() {}
+
+URLFetcher::RequestType AuthorizeAppRequest::GetRequestType() const {
+ return URLFetcher::PUT;
+}
+
+std::vector<std::string>
+AuthorizeAppRequest::GetExtraRequestHeaders() const {
+ std::vector<std::string> headers;
+ headers.push_back(util::kIfMatchAllHeader);
+ return headers;
+}
+
+bool AuthorizeAppRequest::GetContentData(std::string* upload_content_type,
+ std::string* upload_content) {
+ upload_content_type->assign("application/atom+xml");
+ XmlWriter xml_writer;
+ xml_writer.StartWriting();
+ xml_writer.StartElement("entry");
+ xml_writer.AddAttribute("xmlns", "http://www.w3.org/2005/Atom");
+ xml_writer.AddAttribute("xmlns:docs", "http://schemas.google.com/docs/2007");
+ xml_writer.WriteElement("docs:authorizedApp", app_id_);
+
+ xml_writer.EndElement(); // Ends "entry" element.
+ xml_writer.StopWriting();
+ upload_content->assign(xml_writer.GetWrittenString());
+ DVLOG(1) << "AuthorizeAppRequest data: " << *upload_content_type << ", ["
+ << *upload_content << "]";
+ return true;
+}
+
+GURL AuthorizeAppRequest::GetURL() const {
+ return url_generator_.GenerateEditUrl(resource_id_);
+}
+
+//======================= AddResourceToDirectoryRequest ======================
+
+AddResourceToDirectoryRequest::AddResourceToDirectoryRequest(
+ RequestSender* sender,
+ const GDataWapiUrlGenerator& url_generator,
+ const EntryActionCallback& callback,
+ const std::string& parent_resource_id,
+ const std::string& resource_id)
+ : EntryActionRequest(sender, callback),
+ url_generator_(url_generator),
+ parent_resource_id_(parent_resource_id),
+ resource_id_(resource_id) {
+ DCHECK(!callback.is_null());
+}
+
+AddResourceToDirectoryRequest::~AddResourceToDirectoryRequest() {}
+
+GURL AddResourceToDirectoryRequest::GetURL() const {
+ return url_generator_.GenerateContentUrl(parent_resource_id_);
+}
+
+URLFetcher::RequestType
+AddResourceToDirectoryRequest::GetRequestType() const {
+ return URLFetcher::POST;
+}
+
+bool AddResourceToDirectoryRequest::GetContentData(
+ std::string* upload_content_type, std::string* upload_content) {
+ upload_content_type->assign("application/atom+xml");
+ XmlWriter xml_writer;
+ xml_writer.StartWriting();
+ xml_writer.StartElement("entry");
+ xml_writer.AddAttribute("xmlns", "http://www.w3.org/2005/Atom");
+
+ xml_writer.WriteElement(
+ "id", url_generator_.GenerateEditUrlWithoutParams(resource_id_).spec());
+
+ xml_writer.EndElement(); // Ends "entry" element.
+ xml_writer.StopWriting();
+ upload_content->assign(xml_writer.GetWrittenString());
+ DVLOG(1) << "AddResourceToDirectoryRequest data: " << *upload_content_type
+ << ", [" << *upload_content << "]";
+ return true;
+}
+
+//==================== RemoveResourceFromDirectoryRequest ====================
+
+RemoveResourceFromDirectoryRequest::RemoveResourceFromDirectoryRequest(
+ RequestSender* sender,
+ const GDataWapiUrlGenerator& url_generator,
+ const EntryActionCallback& callback,
+ const std::string& parent_resource_id,
+ const std::string& document_resource_id)
+ : EntryActionRequest(sender, callback),
+ url_generator_(url_generator),
+ resource_id_(document_resource_id),
+ parent_resource_id_(parent_resource_id) {
+ DCHECK(!callback.is_null());
+}
+
+RemoveResourceFromDirectoryRequest::~RemoveResourceFromDirectoryRequest() {
+}
+
+GURL RemoveResourceFromDirectoryRequest::GetURL() const {
+ return url_generator_.GenerateResourceUrlForRemoval(
+ parent_resource_id_, resource_id_);
+}
+
+URLFetcher::RequestType
+RemoveResourceFromDirectoryRequest::GetRequestType() const {
+ return URLFetcher::DELETE_REQUEST;
+}
+
+std::vector<std::string>
+RemoveResourceFromDirectoryRequest::GetExtraRequestHeaders() const {
+ std::vector<std::string> headers;
+ headers.push_back(util::kIfMatchAllHeader);
+ return headers;
+}
+
+//======================= InitiateUploadNewFileRequest =======================
+
+InitiateUploadNewFileRequest::InitiateUploadNewFileRequest(
+ RequestSender* sender,
+ const GDataWapiUrlGenerator& url_generator,
+ const InitiateUploadCallback& callback,
+ const std::string& content_type,
+ int64 content_length,
+ const std::string& parent_resource_id,
+ const std::string& title)
+ : InitiateUploadRequestBase(sender, callback, content_type, content_length),
+ url_generator_(url_generator),
+ parent_resource_id_(parent_resource_id),
+ title_(title) {
+}
+
+InitiateUploadNewFileRequest::~InitiateUploadNewFileRequest() {}
+
+GURL InitiateUploadNewFileRequest::GetURL() const {
+ return url_generator_.GenerateInitiateUploadNewFileUrl(parent_resource_id_);
+}
+
+net::URLFetcher::RequestType
+InitiateUploadNewFileRequest::GetRequestType() const {
+ return net::URLFetcher::POST;
+}
+
+bool InitiateUploadNewFileRequest::GetContentData(
+ std::string* upload_content_type,
+ std::string* upload_content) {
+ upload_content_type->assign("application/atom+xml");
+ XmlWriter xml_writer;
+ xml_writer.StartWriting();
+ xml_writer.StartElement("entry");
+ xml_writer.AddAttribute("xmlns", "http://www.w3.org/2005/Atom");
+ xml_writer.AddAttribute("xmlns:docs",
+ "http://schemas.google.com/docs/2007");
+ xml_writer.WriteElement("title", title_);
+ xml_writer.EndElement(); // Ends "entry" element.
+ xml_writer.StopWriting();
+ upload_content->assign(xml_writer.GetWrittenString());
+ DVLOG(1) << "InitiateUploadNewFile: " << *upload_content_type << ", ["
+ << *upload_content << "]";
+ return true;
+}
+
+//===================== InitiateUploadExistingFileRequest ====================
+
+InitiateUploadExistingFileRequest::InitiateUploadExistingFileRequest(
+ RequestSender* sender,
+ const GDataWapiUrlGenerator& url_generator,
+ const InitiateUploadCallback& callback,
+ const std::string& content_type,
+ int64 content_length,
+ const std::string& resource_id,
+ const std::string& etag)
+ : InitiateUploadRequestBase(sender, callback, content_type, content_length),
+ url_generator_(url_generator),
+ resource_id_(resource_id),
+ etag_(etag) {
+}
+
+InitiateUploadExistingFileRequest::~InitiateUploadExistingFileRequest() {}
+
+GURL InitiateUploadExistingFileRequest::GetURL() const {
+ return url_generator_.GenerateInitiateUploadExistingFileUrl(resource_id_);
+}
+
+net::URLFetcher::RequestType
+InitiateUploadExistingFileRequest::GetRequestType() const {
+ return net::URLFetcher::PUT;
+}
+
+bool InitiateUploadExistingFileRequest::GetContentData(
+ std::string* upload_content_type,
+ std::string* upload_content) {
+ // According to the document there is no need to send the content-type.
+ // However, the server would return 500 server error without the
+ // content-type.
+ // As its workaround, send "text/plain" content-type here.
+ *upload_content_type = "text/plain";
+ *upload_content = "";
+ return true;
+}
+
+std::vector<std::string>
+InitiateUploadExistingFileRequest::GetExtraRequestHeaders() const {
+ std::vector<std::string> headers(
+ InitiateUploadRequestBase::GetExtraRequestHeaders());
+ headers.push_back(util::GenerateIfMatchHeader(etag_));
+ return headers;
+}
+
+//============================ ResumeUploadRequest ===========================
+
+ResumeUploadRequest::ResumeUploadRequest(
+ RequestSender* sender,
+ const UploadRangeCallback& callback,
+ const ProgressCallback& progress_callback,
+ const GURL& upload_location,
+ int64 start_position,
+ int64 end_position,
+ int64 content_length,
+ const std::string& content_type,
+ const base::FilePath& local_file_path)
+ : ResumeUploadRequestBase(sender,
+ upload_location,
+ start_position,
+ end_position,
+ content_length,
+ content_type,
+ local_file_path),
+ callback_(callback),
+ progress_callback_(progress_callback) {
+ DCHECK(!callback_.is_null());
+}
+
+ResumeUploadRequest::~ResumeUploadRequest() {}
+
+void ResumeUploadRequest::OnRangeRequestComplete(
+ const UploadRangeResponse& response, scoped_ptr<base::Value> value) {
+ callback_.Run(response, ParseResourceEntry(value.Pass()));
+}
+
+void ResumeUploadRequest::OnURLFetchUploadProgress(
+ const URLFetcher* source, int64 current, int64 total) {
+ if (!progress_callback_.is_null())
+ progress_callback_.Run(current, total);
+}
+
+//========================== GetUploadStatusRequest ==========================
+
+GetUploadStatusRequest::GetUploadStatusRequest(
+ RequestSender* sender,
+ const UploadRangeCallback& callback,
+ const GURL& upload_url,
+ int64 content_length)
+ : GetUploadStatusRequestBase(sender, upload_url, content_length),
+ callback_(callback) {
+ DCHECK(!callback.is_null());
+}
+
+GetUploadStatusRequest::~GetUploadStatusRequest() {}
+
+void GetUploadStatusRequest::OnRangeRequestComplete(
+ const UploadRangeResponse& response, scoped_ptr<base::Value> value) {
+ callback_.Run(response, ParseResourceEntry(value.Pass()));
+}
+
+//========================== DownloadFileRequest ==========================
+
+DownloadFileRequest::DownloadFileRequest(
+ RequestSender* sender,
+ const GDataWapiUrlGenerator& url_generator,
+ const DownloadActionCallback& download_action_callback,
+ const GetContentCallback& get_content_callback,
+ const ProgressCallback& progress_callback,
+ const std::string& resource_id,
+ const base::FilePath& output_file_path)
+ : DownloadFileRequestBase(
+ sender,
+ download_action_callback,
+ get_content_callback,
+ progress_callback,
+ url_generator.GenerateDownloadFileUrl(resource_id),
+ output_file_path) {
+}
+
+DownloadFileRequest::~DownloadFileRequest() {
+}
+
+} // namespace google_apis
diff --git a/chromium/google_apis/drive/gdata_wapi_requests.h b/chromium/google_apis/drive/gdata_wapi_requests.h
new file mode 100644
index 00000000000..a4587d7628b
--- /dev/null
+++ b/chromium/google_apis/drive/gdata_wapi_requests.h
@@ -0,0 +1,486 @@
+// Copyright (c) 2012 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef GOOGLE_APIS_DRIVE_GDATA_WAPI_REQUESTS_H_
+#define GOOGLE_APIS_DRIVE_GDATA_WAPI_REQUESTS_H_
+
+#include <string>
+#include <vector>
+
+#include "google_apis/drive/base_requests.h"
+#include "google_apis/drive/drive_common_callbacks.h"
+#include "google_apis/drive/gdata_wapi_url_generator.h"
+
+namespace google_apis {
+
+class AccountMetadata;
+class GDataWapiUrlGenerator;
+class ResourceEntry;
+
+//============================ GetResourceListRequest ========================
+
+// This class performs the request for fetching a resource list.
+class GetResourceListRequest : public GetDataRequest {
+ public:
+ // override_url:
+ // If empty, a hard-coded base URL of the WAPI server is used to fetch
+ // the first page of the feed. This parameter is used for fetching 2nd
+ // page and onward.
+ //
+ // start_changestamp:
+ // This parameter specifies the starting point of a delta feed or 0 if a
+ // full feed is necessary.
+ //
+ // search_string:
+ // If non-empty, fetches a list of resources that match the search
+ // string.
+ //
+ // directory_resource_id:
+ // If non-empty, fetches a list of resources in a particular directory.
+ //
+ // callback:
+ // Called once the feed is fetched. Must not be null.
+ GetResourceListRequest(RequestSender* sender,
+ const GDataWapiUrlGenerator& url_generator,
+ const GURL& override_url,
+ int64 start_changestamp,
+ const std::string& search_string,
+ const std::string& directory_resource_id,
+ const GetResourceListCallback& callback);
+ virtual ~GetResourceListRequest();
+
+ protected:
+ // UrlFetchRequestBase overrides.
+ virtual GURL GetURL() const OVERRIDE;
+
+ private:
+ const GDataWapiUrlGenerator url_generator_;
+ const GURL override_url_;
+ const int64 start_changestamp_;
+ const std::string search_string_;
+ const std::string directory_resource_id_;
+
+ DISALLOW_COPY_AND_ASSIGN(GetResourceListRequest);
+};
+
+//============================ SearchByTitleRequest ==========================
+
+// This class performs the request for searching resources by title.
+class SearchByTitleRequest : public GetDataRequest {
+ public:
+ // title: the search query.
+ //
+ // directory_resource_id: If given (non-empty), the search target is
+ // directly under the directory with the |directory_resource_id|.
+ // If empty, the search target is all the existing resources.
+ //
+ // callback:
+ // Called once the feed is fetched. Must not be null.
+ SearchByTitleRequest(RequestSender* sender,
+ const GDataWapiUrlGenerator& url_generator,
+ const std::string& title,
+ const std::string& directory_resource_id,
+ const GetResourceListCallback& callback);
+ virtual ~SearchByTitleRequest();
+
+ protected:
+ // UrlFetchRequestBase overrides.
+ virtual GURL GetURL() const OVERRIDE;
+
+ private:
+ const GDataWapiUrlGenerator url_generator_;
+ const std::string title_;
+ const std::string directory_resource_id_;
+
+ DISALLOW_COPY_AND_ASSIGN(SearchByTitleRequest);
+};
+
+//========================= GetResourceEntryRequest ==========================
+
+// This class performs the request for fetching a single resource entry.
+class GetResourceEntryRequest : public GetDataRequest {
+ public:
+ // |callback| must not be null.
+ GetResourceEntryRequest(RequestSender* sender,
+ const GDataWapiUrlGenerator& url_generator,
+ const std::string& resource_id,
+ const GURL& embed_origin,
+ const GetDataCallback& callback);
+ virtual ~GetResourceEntryRequest();
+
+ protected:
+ // UrlFetchRequestBase overrides.
+ virtual GURL GetURL() const OVERRIDE;
+
+ private:
+ const GDataWapiUrlGenerator url_generator_;
+ // Resource id of the requested entry.
+ const std::string resource_id_;
+ // Embed origin for an url to the sharing dialog. Can be empty.
+ const GURL& embed_origin_;
+
+ DISALLOW_COPY_AND_ASSIGN(GetResourceEntryRequest);
+};
+
+//========================= GetAccountMetadataRequest ========================
+
+// Callback used for GetAccountMetadata().
+typedef base::Callback<void(GDataErrorCode error,
+ scoped_ptr<AccountMetadata> account_metadata)>
+ GetAccountMetadataCallback;
+
+// This class performs the request for fetching account metadata.
+class GetAccountMetadataRequest : public GetDataRequest {
+ public:
+ // If |include_installed_apps| is set to true, the result should include
+ // the list of installed third party applications.
+ // |callback| must not be null.
+ GetAccountMetadataRequest(RequestSender* sender,
+ const GDataWapiUrlGenerator& url_generator,
+ const GetAccountMetadataCallback& callback,
+ bool include_installed_apps);
+ virtual ~GetAccountMetadataRequest();
+
+ protected:
+ // UrlFetchRequestBase overrides.
+ virtual GURL GetURL() const OVERRIDE;
+
+ private:
+ const GDataWapiUrlGenerator url_generator_;
+ const bool include_installed_apps_;
+
+ DISALLOW_COPY_AND_ASSIGN(GetAccountMetadataRequest);
+};
+
+//=========================== DeleteResourceRequest ==========================
+
+// This class performs the request for deleting a resource.
+//
+// In WAPI, "gd:deleted" means that the resource was put in the trash, and
+// "docs:removed" means its permanently gone. Since what the class does is to
+// put the resource into trash, we have chosen "Delete" in the name, even though
+// we are preferring the term "Remove" in drive/google_api code.
+class DeleteResourceRequest : public EntryActionRequest {
+ public:
+ // |callback| must not be null.
+ DeleteResourceRequest(RequestSender* sender,
+ const GDataWapiUrlGenerator& url_generator,
+ const EntryActionCallback& callback,
+ const std::string& resource_id,
+ const std::string& etag);
+ virtual ~DeleteResourceRequest();
+
+ protected:
+ // UrlFetchRequestBase overrides.
+ virtual GURL GetURL() const OVERRIDE;
+ virtual net::URLFetcher::RequestType GetRequestType() const OVERRIDE;
+ virtual std::vector<std::string> GetExtraRequestHeaders() const OVERRIDE;
+
+ private:
+ const GDataWapiUrlGenerator url_generator_;
+ const std::string resource_id_;
+ const std::string etag_;
+
+ DISALLOW_COPY_AND_ASSIGN(DeleteResourceRequest);
+};
+
+//========================== CreateDirectoryRequest ==========================
+
+// This class performs the request for creating a directory.
+class CreateDirectoryRequest : public GetDataRequest {
+ public:
+ // A new directory will be created under a directory specified by
+ // |parent_resource_id|. If this parameter is empty, a new directory will
+ // be created in the root directory.
+ // |callback| must not be null.
+ CreateDirectoryRequest(RequestSender* sender,
+ const GDataWapiUrlGenerator& url_generator,
+ const GetDataCallback& callback,
+ const std::string& parent_resource_id,
+ const std::string& directory_title);
+ virtual ~CreateDirectoryRequest();
+
+ protected:
+ // UrlFetchRequestBase overrides.
+ virtual GURL GetURL() const OVERRIDE;
+ virtual net::URLFetcher::RequestType GetRequestType() const OVERRIDE;
+ virtual bool GetContentData(std::string* upload_content_type,
+ std::string* upload_content) OVERRIDE;
+
+ private:
+ const GDataWapiUrlGenerator url_generator_;
+ const std::string parent_resource_id_;
+ const std::string directory_title_;
+
+ DISALLOW_COPY_AND_ASSIGN(CreateDirectoryRequest);
+};
+
+//=========================== RenameResourceRequest ==========================
+
+// This class performs the request for renaming a document/file/directory.
+class RenameResourceRequest : public EntryActionRequest {
+ public:
+ // |callback| must not be null.
+ RenameResourceRequest(RequestSender* sender,
+ const GDataWapiUrlGenerator& url_generator,
+ const EntryActionCallback& callback,
+ const std::string& resource_id,
+ const std::string& new_title);
+ virtual ~RenameResourceRequest();
+
+ protected:
+ // UrlFetchRequestBase overrides.
+ virtual net::URLFetcher::RequestType GetRequestType() const OVERRIDE;
+ virtual std::vector<std::string> GetExtraRequestHeaders() const OVERRIDE;
+ virtual GURL GetURL() const OVERRIDE;
+ virtual bool GetContentData(std::string* upload_content_type,
+ std::string* upload_content) OVERRIDE;
+
+ private:
+ const GDataWapiUrlGenerator url_generator_;
+ const std::string resource_id_;
+ const std::string new_title_;
+
+ DISALLOW_COPY_AND_ASSIGN(RenameResourceRequest);
+};
+
+//=========================== AuthorizeAppRequest ==========================
+
+// This class performs the request for authorizing an application specified
+// by |app_id| to access a document specified by |resource_id|.
+class AuthorizeAppRequest : public GetDataRequest {
+ public:
+ // |callback| must not be null.
+ AuthorizeAppRequest(RequestSender* sender,
+ const GDataWapiUrlGenerator& url_generator,
+ const AuthorizeAppCallback& callback,
+ const std::string& resource_id,
+ const std::string& app_id);
+ virtual ~AuthorizeAppRequest();
+
+ protected:
+ // UrlFetchRequestBase overrides.
+ virtual net::URLFetcher::RequestType GetRequestType() const OVERRIDE;
+ virtual bool GetContentData(std::string* upload_content_type,
+ std::string* upload_content) OVERRIDE;
+ virtual std::vector<std::string> GetExtraRequestHeaders() const OVERRIDE;
+ virtual GURL GetURL() const OVERRIDE;
+
+ private:
+ const GDataWapiUrlGenerator url_generator_;
+ const std::string resource_id_;
+ const std::string app_id_;
+
+ DISALLOW_COPY_AND_ASSIGN(AuthorizeAppRequest);
+};
+
+//======================= AddResourceToDirectoryRequest ======================
+
+// This class performs the request for adding a document/file/directory
+// to a directory.
+class AddResourceToDirectoryRequest : public EntryActionRequest {
+ public:
+ // |callback| must not be null.
+ AddResourceToDirectoryRequest(RequestSender* sender,
+ const GDataWapiUrlGenerator& url_generator,
+ const EntryActionCallback& callback,
+ const std::string& parent_resource_id,
+ const std::string& resource_id);
+ virtual ~AddResourceToDirectoryRequest();
+
+ protected:
+ // UrlFetchRequestBase overrides.
+ virtual GURL GetURL() const OVERRIDE;
+ virtual net::URLFetcher::RequestType GetRequestType() const OVERRIDE;
+ virtual bool GetContentData(std::string* upload_content_type,
+ std::string* upload_content) OVERRIDE;
+
+ private:
+ const GDataWapiUrlGenerator url_generator_;
+ const std::string parent_resource_id_;
+ const std::string resource_id_;
+
+ DISALLOW_COPY_AND_ASSIGN(AddResourceToDirectoryRequest);
+};
+
+//==================== RemoveResourceFromDirectoryRequest ====================
+
+// This class performs the request for removing a document/file/directory
+// from a directory.
+class RemoveResourceFromDirectoryRequest : public EntryActionRequest {
+ public:
+ // |callback| must not be null.
+ RemoveResourceFromDirectoryRequest(RequestSender* sender,
+ const GDataWapiUrlGenerator& url_generator,
+ const EntryActionCallback& callback,
+ const std::string& parent_resource_id,
+ const std::string& resource_id);
+ virtual ~RemoveResourceFromDirectoryRequest();
+
+ protected:
+ // UrlFetchRequestBase overrides.
+ virtual GURL GetURL() const OVERRIDE;
+ virtual net::URLFetcher::RequestType GetRequestType() const OVERRIDE;
+ virtual std::vector<std::string> GetExtraRequestHeaders() const OVERRIDE;
+
+ private:
+ const GDataWapiUrlGenerator url_generator_;
+ const std::string resource_id_;
+ const std::string parent_resource_id_;
+
+ DISALLOW_COPY_AND_ASSIGN(RemoveResourceFromDirectoryRequest);
+};
+
+//======================= InitiateUploadNewFileRequest =======================
+
+// This class performs the request for initiating the upload of a new file.
+class InitiateUploadNewFileRequest : public InitiateUploadRequestBase {
+ public:
+ // |title| should be set.
+ // |parent_upload_url| should be the upload_url() of the parent directory.
+ // (resumable-create-media URL)
+ // See also the comments of InitiateUploadRequestBase for more details
+ // about the other parameters.
+ InitiateUploadNewFileRequest(RequestSender* sender,
+ const GDataWapiUrlGenerator& url_generator,
+ const InitiateUploadCallback& callback,
+ const std::string& content_type,
+ int64 content_length,
+ const std::string& parent_resource_id,
+ const std::string& title);
+ virtual ~InitiateUploadNewFileRequest();
+
+ protected:
+ // UrlFetchRequestBase overrides.
+ virtual GURL GetURL() const OVERRIDE;
+ virtual net::URLFetcher::RequestType GetRequestType() const OVERRIDE;
+ virtual bool GetContentData(std::string* upload_content_type,
+ std::string* upload_content) OVERRIDE;
+
+ private:
+ const GDataWapiUrlGenerator url_generator_;
+ const std::string parent_resource_id_;
+ const std::string title_;
+
+ DISALLOW_COPY_AND_ASSIGN(InitiateUploadNewFileRequest);
+};
+
+//==================== InitiateUploadExistingFileRequest =====================
+
+// This class performs the request for initiating the upload of an existing
+// file.
+class InitiateUploadExistingFileRequest
+ : public InitiateUploadRequestBase {
+ public:
+ // |upload_url| should be the upload_url() of the file
+ // (resumable-create-media URL)
+ // |etag| should be set if it is available to detect the upload confliction.
+ // See also the comments of InitiateUploadRequestBase for more details
+ // about the other parameters.
+ InitiateUploadExistingFileRequest(RequestSender* sender,
+ const GDataWapiUrlGenerator& url_generator,
+ const InitiateUploadCallback& callback,
+ const std::string& content_type,
+ int64 content_length,
+ const std::string& resource_id,
+ const std::string& etag);
+ virtual ~InitiateUploadExistingFileRequest();
+
+ protected:
+ // UrlFetchRequestBase overrides.
+ virtual GURL GetURL() const OVERRIDE;
+ virtual net::URLFetcher::RequestType GetRequestType() const OVERRIDE;
+ virtual std::vector<std::string> GetExtraRequestHeaders() const OVERRIDE;
+ virtual bool GetContentData(std::string* upload_content_type,
+ std::string* upload_content) OVERRIDE;
+
+ private:
+ const GDataWapiUrlGenerator url_generator_;
+ const std::string resource_id_;
+ const std::string etag_;
+
+ DISALLOW_COPY_AND_ASSIGN(InitiateUploadExistingFileRequest);
+};
+
+//============================ ResumeUploadRequest ===========================
+
+// Performs the request for resuming the upload of a file.
+class ResumeUploadRequest : public ResumeUploadRequestBase {
+ public:
+ // See also ResumeUploadRequestBase's comment for parameters meaning.
+ // |callback| must not be null.
+ ResumeUploadRequest(RequestSender* sender,
+ const UploadRangeCallback& callback,
+ const ProgressCallback& progress_callback,
+ const GURL& upload_location,
+ int64 start_position,
+ int64 end_position,
+ int64 content_length,
+ const std::string& content_type,
+ const base::FilePath& local_file_path);
+ virtual ~ResumeUploadRequest();
+
+ protected:
+ // UploadRangeRequestBase overrides.
+ virtual void OnRangeRequestComplete(
+ const UploadRangeResponse& response,
+ scoped_ptr<base::Value> value) OVERRIDE;
+ // content::UrlFetcherDelegate overrides.
+ virtual void OnURLFetchUploadProgress(const net::URLFetcher* source,
+ int64 current, int64 total) OVERRIDE;
+
+ private:
+ const UploadRangeCallback callback_;
+ const ProgressCallback progress_callback_;
+
+ DISALLOW_COPY_AND_ASSIGN(ResumeUploadRequest);
+};
+
+//========================== GetUploadStatusRequest ==========================
+
+// Performs the request to request the current upload status of a file.
+class GetUploadStatusRequest : public GetUploadStatusRequestBase {
+ public:
+ // See also GetUploadStatusRequestBase's comment for parameters meaning.
+ // |callback| must not be null.
+ GetUploadStatusRequest(RequestSender* sender,
+ const UploadRangeCallback& callback,
+ const GURL& upload_url,
+ int64 content_length);
+ virtual ~GetUploadStatusRequest();
+
+ protected:
+ // UploadRangeRequestBase overrides.
+ virtual void OnRangeRequestComplete(
+ const UploadRangeResponse& response,
+ scoped_ptr<base::Value> value) OVERRIDE;
+
+ private:
+ const UploadRangeCallback callback_;
+
+ DISALLOW_COPY_AND_ASSIGN(GetUploadStatusRequest);
+};
+
+
+//========================== DownloadFileRequest ==========================
+
+// This class performs the request for downloading of a specified file.
+class DownloadFileRequest : public DownloadFileRequestBase {
+ public:
+ // See also DownloadFileRequestBase's comment for parameters meaning.
+ DownloadFileRequest(RequestSender* sender,
+ const GDataWapiUrlGenerator& url_generator,
+ const DownloadActionCallback& download_action_callback,
+ const GetContentCallback& get_content_callback,
+ const ProgressCallback& progress_callback,
+ const std::string& resource_id,
+ const base::FilePath& output_file_path);
+ virtual ~DownloadFileRequest();
+
+ DISALLOW_COPY_AND_ASSIGN(DownloadFileRequest);
+};
+
+} // namespace google_apis
+
+#endif // GOOGLE_APIS_DRIVE_GDATA_WAPI_REQUESTS_H_
diff --git a/chromium/google_apis/drive/gdata_wapi_requests_unittest.cc b/chromium/google_apis/drive/gdata_wapi_requests_unittest.cc
new file mode 100644
index 00000000000..e302f23f1e8
--- /dev/null
+++ b/chromium/google_apis/drive/gdata_wapi_requests_unittest.cc
@@ -0,0 +1,1561 @@
+// Copyright (c) 2012 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include <algorithm>
+#include <map>
+
+#include "base/bind.h"
+#include "base/file_util.h"
+#include "base/files/file_path.h"
+#include "base/files/scoped_temp_dir.h"
+#include "base/json/json_reader.h"
+#include "base/json/json_writer.h"
+#include "base/message_loop/message_loop.h"
+#include "base/run_loop.h"
+#include "base/strings/string_number_conversions.h"
+#include "base/strings/stringprintf.h"
+#include "base/values.h"
+#include "google_apis/drive/dummy_auth_service.h"
+#include "google_apis/drive/gdata_wapi_parser.h"
+#include "google_apis/drive/gdata_wapi_requests.h"
+#include "google_apis/drive/gdata_wapi_url_generator.h"
+#include "google_apis/drive/request_sender.h"
+#include "google_apis/drive/test_util.h"
+#include "net/base/escape.h"
+#include "net/test/embedded_test_server/embedded_test_server.h"
+#include "net/test/embedded_test_server/http_request.h"
+#include "net/test/embedded_test_server/http_response.h"
+#include "net/url_request/url_request_test_util.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+namespace google_apis {
+
+namespace {
+
+const char kTestUserAgent[] = "test-user-agent";
+const char kTestETag[] = "test_etag";
+const char kTestDownloadPathPrefix[] = "/download/";
+
+class GDataWapiRequestsTest : public testing::Test {
+ public:
+ GDataWapiRequestsTest() {
+ }
+
+ virtual void SetUp() OVERRIDE {
+ request_context_getter_ = new net::TestURLRequestContextGetter(
+ message_loop_.message_loop_proxy());
+
+ request_sender_.reset(new RequestSender(new DummyAuthService,
+ request_context_getter_.get(),
+ message_loop_.message_loop_proxy(),
+ kTestUserAgent));
+
+ ASSERT_TRUE(temp_dir_.CreateUniqueTempDir());
+
+ ASSERT_TRUE(test_server_.InitializeAndWaitUntilReady());
+ test_server_.RegisterRequestHandler(
+ base::Bind(&test_util::HandleDownloadFileRequest,
+ test_server_.base_url(),
+ base::Unretained(&http_request_)));
+ test_server_.RegisterRequestHandler(
+ base::Bind(&GDataWapiRequestsTest::HandleResourceFeedRequest,
+ base::Unretained(this)));
+ test_server_.RegisterRequestHandler(
+ base::Bind(&GDataWapiRequestsTest::HandleMetadataRequest,
+ base::Unretained(this)));
+ test_server_.RegisterRequestHandler(
+ base::Bind(&GDataWapiRequestsTest::HandleCreateSessionRequest,
+ base::Unretained(this)));
+ test_server_.RegisterRequestHandler(
+ base::Bind(&GDataWapiRequestsTest::HandleUploadRequest,
+ base::Unretained(this)));
+ test_server_.RegisterRequestHandler(
+ base::Bind(&GDataWapiRequestsTest::HandleDownloadRequest,
+ base::Unretained(this)));
+
+ GURL test_base_url = test_util::GetBaseUrlForTesting(test_server_.port());
+ url_generator_.reset(new GDataWapiUrlGenerator(
+ test_base_url, test_base_url.Resolve(kTestDownloadPathPrefix)));
+
+ received_bytes_ = 0;
+ content_length_ = 0;
+ }
+
+ protected:
+ // Handles a request for fetching a resource feed.
+ scoped_ptr<net::test_server::HttpResponse> HandleResourceFeedRequest(
+ const net::test_server::HttpRequest& request) {
+ http_request_ = request;
+
+ const GURL absolute_url = test_server_.GetURL(request.relative_url);
+ std::string remaining_path;
+ if (absolute_url.path() == "/feeds/default/private/full" &&
+ request.method == net::test_server::METHOD_POST) {
+ // This is a request for copying a document.
+ // TODO(satorux): we should generate valid JSON data for the newly
+ // copied document but for now, just return "file_entry.json"
+ scoped_ptr<net::test_server::BasicHttpResponse> result(
+ test_util::CreateHttpResponseFromFile(
+ test_util::GetTestFilePath("gdata/file_entry.json")));
+ return result.PassAs<net::test_server::HttpResponse>();
+ }
+
+ if (!test_util::RemovePrefix(absolute_url.path(),
+ "/feeds/default/private/full",
+ &remaining_path)) {
+ return scoped_ptr<net::test_server::HttpResponse>();
+ }
+
+ if (remaining_path.empty()) {
+ // Process the default feed.
+ scoped_ptr<net::test_server::BasicHttpResponse> result(
+ test_util::CreateHttpResponseFromFile(
+ test_util::GetTestFilePath("gdata/root_feed.json")));
+ return result.PassAs<net::test_server::HttpResponse>();
+ } else {
+ // Process a feed for a single resource ID.
+ const std::string resource_id = net::UnescapeURLComponent(
+ remaining_path.substr(1), net::UnescapeRule::URL_SPECIAL_CHARS);
+ if (resource_id == "file:2_file_resource_id") {
+ scoped_ptr<net::test_server::BasicHttpResponse> result(
+ test_util::CreateHttpResponseFromFile(
+ test_util::GetTestFilePath("gdata/file_entry.json")));
+ return result.PassAs<net::test_server::HttpResponse>();
+ } else if (resource_id == "folder:root/contents" &&
+ request.method == net::test_server::METHOD_POST) {
+ // This is a request for creating a directory in the root directory.
+ // TODO(satorux): we should generate valid JSON data for the newly
+ // created directory but for now, just return "directory_entry.json"
+ scoped_ptr<net::test_server::BasicHttpResponse> result(
+ test_util::CreateHttpResponseFromFile(
+ test_util::GetTestFilePath(
+ "gdata/directory_entry.json")));
+ return result.PassAs<net::test_server::HttpResponse>();
+ } else if (resource_id ==
+ "folder:root/contents/file:2_file_resource_id" &&
+ request.method == net::test_server::METHOD_DELETE) {
+ // This is a request for deleting a file from the root directory.
+ // TODO(satorux): Investigate what's returned from the server, and
+ // copy it. For now, just return a random file, as the contents don't
+ // matter.
+ scoped_ptr<net::test_server::BasicHttpResponse> result(
+ test_util::CreateHttpResponseFromFile(
+ test_util::GetTestFilePath("gdata/testfile.txt")));
+ return result.PassAs<net::test_server::HttpResponse>();
+ } else if (resource_id == "invalid_resource_id") {
+ // Check if this is an authorization request for an app.
+ // This emulates to return invalid formatted result from the server.
+ if (request.method == net::test_server::METHOD_PUT &&
+ request.content.find("<docs:authorizedApp>") != std::string::npos) {
+ scoped_ptr<net::test_server::BasicHttpResponse> result(
+ test_util::CreateHttpResponseFromFile(
+ test_util::GetTestFilePath("gdata/testfile.txt")));
+ return result.PassAs<net::test_server::HttpResponse>();
+ }
+ }
+ }
+
+ return scoped_ptr<net::test_server::HttpResponse>();
+ }
+
+ // Handles a request for fetching a metadata feed.
+ scoped_ptr<net::test_server::HttpResponse> HandleMetadataRequest(
+ const net::test_server::HttpRequest& request) {
+ http_request_ = request;
+
+ const GURL absolute_url = test_server_.GetURL(request.relative_url);
+ if (absolute_url.path() != "/feeds/metadata/default")
+ return scoped_ptr<net::test_server::HttpResponse>();
+
+ scoped_ptr<net::test_server::BasicHttpResponse> result(
+ test_util::CreateHttpResponseFromFile(
+ test_util::GetTestFilePath(
+ "gdata/account_metadata.json")));
+ if (absolute_url.query().find("include-installed-apps=true") ==
+ string::npos) {
+ // Exclude the list of installed apps.
+ scoped_ptr<base::Value> parsed_content(
+ base::JSONReader::Read(result->content(), base::JSON_PARSE_RFC));
+ CHECK(parsed_content);
+
+ // Remove the install apps node.
+ base::DictionaryValue* dictionary_value;
+ CHECK(parsed_content->GetAsDictionary(&dictionary_value));
+ dictionary_value->Remove("entry.docs$installedApp", NULL);
+
+ // Write back it as the content of the result.
+ std::string content;
+ base::JSONWriter::Write(parsed_content.get(), &content);
+ result->set_content(content);
+ }
+
+ return result.PassAs<net::test_server::HttpResponse>();
+ }
+
+ // Handles a request for creating a session for uploading.
+ scoped_ptr<net::test_server::HttpResponse> HandleCreateSessionRequest(
+ const net::test_server::HttpRequest& request) {
+ http_request_ = request;
+
+ const GURL absolute_url = test_server_.GetURL(request.relative_url);
+ if (StartsWithASCII(absolute_url.path(),
+ "/feeds/upload/create-session/default/private/full",
+ true)) { // case sensitive
+ // This is an initiating upload URL.
+ scoped_ptr<net::test_server::BasicHttpResponse> http_response(
+ new net::test_server::BasicHttpResponse);
+
+ // Check an ETag.
+ std::map<std::string, std::string>::const_iterator found =
+ request.headers.find("If-Match");
+ if (found != request.headers.end() &&
+ found->second != "*" &&
+ found->second != kTestETag) {
+ http_response->set_code(net::HTTP_PRECONDITION_FAILED);
+ return http_response.PassAs<net::test_server::HttpResponse>();
+ }
+
+ // Check if the X-Upload-Content-Length is present. If yes, store the
+ // length of the file.
+ found = request.headers.find("X-Upload-Content-Length");
+ if (found == request.headers.end() ||
+ !base::StringToInt64(found->second, &content_length_)) {
+ return scoped_ptr<net::test_server::HttpResponse>();
+ }
+ received_bytes_ = 0;
+
+ http_response->set_code(net::HTTP_OK);
+ GURL upload_url;
+ // POST is used for a new file, and PUT is used for an existing file.
+ if (request.method == net::test_server::METHOD_POST) {
+ upload_url = test_server_.GetURL("/upload_new_file");
+ } else if (request.method == net::test_server::METHOD_PUT) {
+ upload_url = test_server_.GetURL("/upload_existing_file");
+ } else {
+ return scoped_ptr<net::test_server::HttpResponse>();
+ }
+ http_response->AddCustomHeader("Location", upload_url.spec());
+ return http_response.PassAs<net::test_server::HttpResponse>();
+ }
+
+ return scoped_ptr<net::test_server::HttpResponse>();
+ }
+
+ // Handles a request for uploading content.
+ scoped_ptr<net::test_server::HttpResponse> HandleUploadRequest(
+ const net::test_server::HttpRequest& request) {
+ http_request_ = request;
+
+ const GURL absolute_url = test_server_.GetURL(request.relative_url);
+ if (absolute_url.path() != "/upload_new_file" &&
+ absolute_url.path() != "/upload_existing_file") {
+ return scoped_ptr<net::test_server::HttpResponse>();
+ }
+
+ // TODO(satorux): We should create a correct JSON data for the uploaded
+ // file, but for now, just return file_entry.json.
+ scoped_ptr<net::test_server::BasicHttpResponse> response =
+ test_util::CreateHttpResponseFromFile(
+ test_util::GetTestFilePath("gdata/file_entry.json"));
+ // response.code() is set to SUCCESS. Change it to CREATED if it's a new
+ // file.
+ if (absolute_url.path() == "/upload_new_file")
+ response->set_code(net::HTTP_CREATED);
+
+ // Check if the Content-Range header is present. This must be present if
+ // the request body is not empty.
+ if (!request.content.empty()) {
+ std::map<std::string, std::string>::const_iterator iter =
+ request.headers.find("Content-Range");
+ if (iter == request.headers.end())
+ return scoped_ptr<net::test_server::HttpResponse>();
+ int64 length = 0;
+ int64 start_position = 0;
+ int64 end_position = 0;
+ if (!test_util::ParseContentRangeHeader(iter->second,
+ &start_position,
+ &end_position,
+ &length)) {
+ return scoped_ptr<net::test_server::HttpResponse>();
+ }
+ EXPECT_EQ(start_position, received_bytes_);
+ EXPECT_EQ(length, content_length_);
+ // end_position is inclusive, but so +1 to change the range to byte size.
+ received_bytes_ = end_position + 1;
+ }
+
+ // Add Range header to the response, based on the values of
+ // Content-Range header in the request.
+ // The header is annotated only when at least one byte is received.
+ if (received_bytes_ > 0) {
+ response->AddCustomHeader(
+ "Range",
+ "bytes=0-" + base::Int64ToString(received_bytes_ - 1));
+ }
+
+ // Change the code to RESUME_INCOMPLETE if upload is not complete.
+ if (received_bytes_ < content_length_)
+ response->set_code(static_cast<net::HttpStatusCode>(308));
+
+ return response.PassAs<net::test_server::HttpResponse>();
+ }
+
+ // Handles a request for downloading a file.
+ scoped_ptr<net::test_server::HttpResponse> HandleDownloadRequest(
+ const net::test_server::HttpRequest& request) {
+ http_request_ = request;
+
+ const GURL absolute_url = test_server_.GetURL(request.relative_url);
+ std::string id;
+ if (!test_util::RemovePrefix(absolute_url.path(),
+ kTestDownloadPathPrefix,
+ &id)) {
+ return scoped_ptr<net::test_server::HttpResponse>();
+ }
+
+ // For testing, returns a text with |id| repeated 3 times.
+ scoped_ptr<net::test_server::BasicHttpResponse> response(
+ new net::test_server::BasicHttpResponse);
+ response->set_code(net::HTTP_OK);
+ response->set_content(id + id + id);
+ response->set_content_type("text/plain");
+ return response.PassAs<net::test_server::HttpResponse>();
+ }
+
+ base::MessageLoopForIO message_loop_; // Test server needs IO thread.
+ net::test_server::EmbeddedTestServer test_server_;
+ scoped_ptr<RequestSender> request_sender_;
+ scoped_ptr<GDataWapiUrlGenerator> url_generator_;
+ scoped_refptr<net::TestURLRequestContextGetter> request_context_getter_;
+ base::ScopedTempDir temp_dir_;
+
+ // These fields are used to keep the current upload state during a
+ // test case. These values are updated by the request from
+ // ResumeUploadRequest, and used to construct the response for
+ // both ResumeUploadRequest and GetUploadStatusRequest, to emulate
+ // the WAPI server.
+ int64 received_bytes_;
+ int64 content_length_;
+
+ // The incoming HTTP request is saved so tests can verify the request
+ // parameters like HTTP method (ex. some requests should use DELETE
+ // instead of GET).
+ net::test_server::HttpRequest http_request_;
+};
+
+} // namespace
+
+TEST_F(GDataWapiRequestsTest, GetResourceListRequest_DefaultFeed) {
+ GDataErrorCode result_code = GDATA_OTHER_ERROR;
+ scoped_ptr<ResourceList> result_data;
+
+ {
+ base::RunLoop run_loop;
+ GetResourceListRequest* request = new GetResourceListRequest(
+ request_sender_.get(),
+ *url_generator_,
+ GURL(), // Pass an empty URL to use the default feed
+ 0, // start changestamp
+ std::string(), // search string
+ std::string(), // directory resource ID
+ test_util::CreateQuitCallback(
+ &run_loop,
+ test_util::CreateCopyResultCallback(&result_code, &result_data)));
+ request_sender_->StartRequestWithRetry(request);
+ run_loop.Run();
+ }
+
+ EXPECT_EQ(HTTP_SUCCESS, result_code);
+ EXPECT_EQ(net::test_server::METHOD_GET, http_request_.method);
+ EXPECT_EQ("/feeds/default/private/full?v=3&alt=json&showroot=true&"
+ "showfolders=true&include-shared=true&max-results=500",
+ http_request_.relative_url);
+
+ // Sanity check of the result.
+ scoped_ptr<ResourceList> expected(
+ ResourceList::ExtractAndParse(
+ *test_util::LoadJSONFile("gdata/root_feed.json")));
+ ASSERT_TRUE(result_data);
+ EXPECT_EQ(expected->title(), result_data->title());
+}
+
+TEST_F(GDataWapiRequestsTest, GetResourceListRequest_ValidFeed) {
+ GDataErrorCode result_code = GDATA_OTHER_ERROR;
+ scoped_ptr<ResourceList> result_data;
+
+ {
+ base::RunLoop run_loop;
+ GetResourceListRequest* request = new GetResourceListRequest(
+ request_sender_.get(),
+ *url_generator_,
+ test_server_.GetURL("/files/gdata/root_feed.json"),
+ 0, // start changestamp
+ std::string(), // search string
+ std::string(), // directory resource ID
+ test_util::CreateQuitCallback(
+ &run_loop,
+ test_util::CreateCopyResultCallback(&result_code, &result_data)));
+ request_sender_->StartRequestWithRetry(request);
+ run_loop.Run();
+ }
+
+ EXPECT_EQ(HTTP_SUCCESS, result_code);
+ EXPECT_EQ(net::test_server::METHOD_GET, http_request_.method);
+ EXPECT_EQ("/files/gdata/root_feed.json?v=3&alt=json&showroot=true&"
+ "showfolders=true&include-shared=true&max-results=500",
+ http_request_.relative_url);
+
+ scoped_ptr<ResourceList> expected(
+ ResourceList::ExtractAndParse(
+ *test_util::LoadJSONFile("gdata/root_feed.json")));
+ ASSERT_TRUE(result_data);
+ EXPECT_EQ(expected->title(), result_data->title());
+}
+
+TEST_F(GDataWapiRequestsTest, GetResourceListRequest_InvalidFeed) {
+ // testfile.txt exists but the response is not JSON, so it should
+ // emit a parse error instead.
+ GDataErrorCode result_code = GDATA_OTHER_ERROR;
+ scoped_ptr<ResourceList> result_data;
+
+ {
+ base::RunLoop run_loop;
+ GetResourceListRequest* request = new GetResourceListRequest(
+ request_sender_.get(),
+ *url_generator_,
+ test_server_.GetURL("/files/gdata/testfile.txt"),
+ 0, // start changestamp
+ std::string(), // search string
+ std::string(), // directory resource ID
+ test_util::CreateQuitCallback(
+ &run_loop,
+ test_util::CreateCopyResultCallback(&result_code, &result_data)));
+ request_sender_->StartRequestWithRetry(request);
+ run_loop.Run();
+ }
+
+ EXPECT_EQ(GDATA_PARSE_ERROR, result_code);
+ EXPECT_EQ(net::test_server::METHOD_GET, http_request_.method);
+ EXPECT_EQ("/files/gdata/testfile.txt?v=3&alt=json&showroot=true&"
+ "showfolders=true&include-shared=true&max-results=500",
+ http_request_.relative_url);
+ EXPECT_FALSE(result_data);
+}
+
+TEST_F(GDataWapiRequestsTest, SearchByTitleRequest) {
+ GDataErrorCode result_code = GDATA_OTHER_ERROR;
+ scoped_ptr<ResourceList> result_data;
+
+ {
+ base::RunLoop run_loop;
+ SearchByTitleRequest* request = new SearchByTitleRequest(
+ request_sender_.get(),
+ *url_generator_,
+ "search-title",
+ std::string(), // directory resource id
+ test_util::CreateQuitCallback(
+ &run_loop,
+ test_util::CreateCopyResultCallback(&result_code, &result_data)));
+ request_sender_->StartRequestWithRetry(request);
+ run_loop.Run();
+ }
+
+ EXPECT_EQ(HTTP_SUCCESS, result_code);
+ EXPECT_EQ(net::test_server::METHOD_GET, http_request_.method);
+ EXPECT_EQ("/feeds/default/private/full?v=3&alt=json&showroot=true&"
+ "showfolders=true&include-shared=true&max-results=500"
+ "&title=search-title&title-exact=true",
+ http_request_.relative_url);
+ EXPECT_TRUE(result_data);
+}
+
+TEST_F(GDataWapiRequestsTest, GetResourceEntryRequest_ValidResourceId) {
+ GDataErrorCode result_code = GDATA_OTHER_ERROR;
+ scoped_ptr<base::Value> result_data;
+
+ {
+ base::RunLoop run_loop;
+ GetResourceEntryRequest* request = new GetResourceEntryRequest(
+ request_sender_.get(),
+ *url_generator_,
+ "file:2_file_resource_id", // resource ID
+ GURL(), // embed origin
+ test_util::CreateQuitCallback(
+ &run_loop,
+ test_util::CreateCopyResultCallback(&result_code, &result_data)));
+ request_sender_->StartRequestWithRetry(request);
+ run_loop.Run();
+ }
+
+ EXPECT_EQ(HTTP_SUCCESS, result_code);
+ EXPECT_EQ(net::test_server::METHOD_GET, http_request_.method);
+ EXPECT_EQ("/feeds/default/private/full/file%3A2_file_resource_id"
+ "?v=3&alt=json&showroot=true",
+ http_request_.relative_url);
+ scoped_ptr<base::Value> expected_json =
+ test_util::LoadJSONFile("gdata/file_entry.json");
+ ASSERT_TRUE(expected_json);
+ EXPECT_TRUE(result_data);
+ EXPECT_TRUE(base::Value::Equals(expected_json.get(), result_data.get()));
+}
+
+TEST_F(GDataWapiRequestsTest, GetResourceEntryRequest_InvalidResourceId) {
+ GDataErrorCode result_code = GDATA_OTHER_ERROR;
+ scoped_ptr<base::Value> result_data;
+
+ {
+ base::RunLoop run_loop;
+ GetResourceEntryRequest* request = new GetResourceEntryRequest(
+ request_sender_.get(),
+ *url_generator_,
+ "<invalid>", // resource ID
+ GURL(), // embed origin
+ test_util::CreateQuitCallback(
+ &run_loop,
+ test_util::CreateCopyResultCallback(&result_code, &result_data)));
+ request_sender_->StartRequestWithRetry(request);
+ run_loop.Run();
+ }
+
+ EXPECT_EQ(HTTP_NOT_FOUND, result_code);
+ EXPECT_EQ(net::test_server::METHOD_GET, http_request_.method);
+ EXPECT_EQ("/feeds/default/private/full/%3Cinvalid%3E?v=3&alt=json"
+ "&showroot=true",
+ http_request_.relative_url);
+ ASSERT_FALSE(result_data);
+}
+
+TEST_F(GDataWapiRequestsTest, GetAccountMetadataRequest) {
+ GDataErrorCode result_code = GDATA_OTHER_ERROR;
+ scoped_ptr<AccountMetadata> result_data;
+
+ {
+ base::RunLoop run_loop;
+ GetAccountMetadataRequest* request = new GetAccountMetadataRequest(
+ request_sender_.get(),
+ *url_generator_,
+ test_util::CreateQuitCallback(
+ &run_loop,
+ test_util::CreateCopyResultCallback(&result_code, &result_data)),
+ true); // Include installed apps.
+ request_sender_->StartRequestWithRetry(request);
+ run_loop.Run();
+ }
+
+ EXPECT_EQ(HTTP_SUCCESS, result_code);
+ EXPECT_EQ(net::test_server::METHOD_GET, http_request_.method);
+ EXPECT_EQ("/feeds/metadata/default?v=3&alt=json&showroot=true"
+ "&include-installed-apps=true",
+ http_request_.relative_url);
+
+ scoped_ptr<AccountMetadata> expected(
+ AccountMetadata::CreateFrom(
+ *test_util::LoadJSONFile("gdata/account_metadata.json")));
+
+ ASSERT_TRUE(result_data.get());
+ EXPECT_EQ(expected->largest_changestamp(),
+ result_data->largest_changestamp());
+ EXPECT_EQ(expected->quota_bytes_total(),
+ result_data->quota_bytes_total());
+ EXPECT_EQ(expected->quota_bytes_used(),
+ result_data->quota_bytes_used());
+
+ // Sanity check for installed apps.
+ EXPECT_EQ(expected->installed_apps().size(),
+ result_data->installed_apps().size());
+}
+
+TEST_F(GDataWapiRequestsTest,
+ GetAccountMetadataRequestWithoutInstalledApps) {
+ GDataErrorCode result_code = GDATA_OTHER_ERROR;
+ scoped_ptr<AccountMetadata> result_data;
+
+ {
+ base::RunLoop run_loop;
+ GetAccountMetadataRequest* request = new GetAccountMetadataRequest(
+ request_sender_.get(),
+ *url_generator_,
+ test_util::CreateQuitCallback(
+ &run_loop,
+ test_util::CreateCopyResultCallback(&result_code, &result_data)),
+ false); // Exclude installed apps.
+ request_sender_->StartRequestWithRetry(request);
+ run_loop.Run();
+ }
+
+ EXPECT_EQ(HTTP_SUCCESS, result_code);
+ EXPECT_EQ(net::test_server::METHOD_GET, http_request_.method);
+ EXPECT_EQ("/feeds/metadata/default?v=3&alt=json&showroot=true",
+ http_request_.relative_url);
+
+ scoped_ptr<AccountMetadata> expected(
+ AccountMetadata::CreateFrom(
+ *test_util::LoadJSONFile("gdata/account_metadata.json")));
+
+ ASSERT_TRUE(result_data.get());
+ EXPECT_EQ(expected->largest_changestamp(),
+ result_data->largest_changestamp());
+ EXPECT_EQ(expected->quota_bytes_total(),
+ result_data->quota_bytes_total());
+ EXPECT_EQ(expected->quota_bytes_used(),
+ result_data->quota_bytes_used());
+
+ // Installed apps shouldn't be included.
+ EXPECT_EQ(0U, result_data->installed_apps().size());
+}
+
+TEST_F(GDataWapiRequestsTest, DeleteResourceRequest) {
+ GDataErrorCode result_code = GDATA_OTHER_ERROR;
+
+ {
+ base::RunLoop run_loop;
+ DeleteResourceRequest* request = new DeleteResourceRequest(
+ request_sender_.get(),
+ *url_generator_,
+ test_util::CreateQuitCallback(
+ &run_loop,
+ test_util::CreateCopyResultCallback(&result_code)),
+ "file:2_file_resource_id",
+ std::string());
+
+ request_sender_->StartRequestWithRetry(request);
+ run_loop.Run();
+ }
+
+ EXPECT_EQ(HTTP_SUCCESS, result_code);
+ EXPECT_EQ(net::test_server::METHOD_DELETE, http_request_.method);
+ EXPECT_EQ(
+ "/feeds/default/private/full/file%3A2_file_resource_id?v=3&alt=json"
+ "&showroot=true",
+ http_request_.relative_url);
+ EXPECT_EQ("*", http_request_.headers["If-Match"]);
+}
+
+TEST_F(GDataWapiRequestsTest, DeleteResourceRequestWithETag) {
+ GDataErrorCode result_code = GDATA_OTHER_ERROR;
+
+ {
+ base::RunLoop run_loop;
+ DeleteResourceRequest* request = new DeleteResourceRequest(
+ request_sender_.get(),
+ *url_generator_,
+ test_util::CreateQuitCallback(
+ &run_loop,
+ test_util::CreateCopyResultCallback(&result_code)),
+ "file:2_file_resource_id",
+ "etag");
+
+ request_sender_->StartRequestWithRetry(request);
+ run_loop.Run();
+ }
+
+ EXPECT_EQ(HTTP_SUCCESS, result_code);
+ EXPECT_EQ(net::test_server::METHOD_DELETE, http_request_.method);
+ EXPECT_EQ(
+ "/feeds/default/private/full/file%3A2_file_resource_id?v=3&alt=json"
+ "&showroot=true",
+ http_request_.relative_url);
+ EXPECT_EQ("etag", http_request_.headers["If-Match"]);
+}
+
+TEST_F(GDataWapiRequestsTest, CreateDirectoryRequest) {
+ GDataErrorCode result_code = GDATA_OTHER_ERROR;
+ scoped_ptr<base::Value> result_data;
+
+ // Create "new directory" in the root directory.
+ {
+ base::RunLoop run_loop;
+ CreateDirectoryRequest* request = new CreateDirectoryRequest(
+ request_sender_.get(),
+ *url_generator_,
+ test_util::CreateQuitCallback(
+ &run_loop,
+ test_util::CreateCopyResultCallback(&result_code, &result_data)),
+ "folder:root",
+ "new directory");
+
+ request_sender_->StartRequestWithRetry(request);
+ run_loop.Run();
+ }
+
+ EXPECT_EQ(HTTP_SUCCESS, result_code);
+ EXPECT_EQ(net::test_server::METHOD_POST, http_request_.method);
+ EXPECT_EQ("/feeds/default/private/full/folder%3Aroot/contents?v=3&alt=json"
+ "&showroot=true",
+ http_request_.relative_url);
+ EXPECT_EQ("application/atom+xml", http_request_.headers["Content-Type"]);
+
+ EXPECT_TRUE(http_request_.has_content);
+ EXPECT_EQ("<?xml version=\"1.0\"?>\n"
+ "<entry xmlns=\"http://www.w3.org/2005/Atom\">\n"
+ " <category scheme=\"http://schemas.google.com/g/2005#kind\" "
+ "term=\"http://schemas.google.com/docs/2007#folder\"/>\n"
+ " <title>new directory</title>\n"
+ "</entry>\n",
+ http_request_.content);
+}
+
+TEST_F(GDataWapiRequestsTest, RenameResourceRequest) {
+ GDataErrorCode result_code = GDATA_OTHER_ERROR;
+
+ // Rename a file with a new name "New File".
+ {
+ base::RunLoop run_loop;
+ RenameResourceRequest* request = new RenameResourceRequest(
+ request_sender_.get(),
+ *url_generator_,
+ test_util::CreateQuitCallback(
+ &run_loop,
+ test_util::CreateCopyResultCallback(&result_code)),
+ "file:2_file_resource_id",
+ "New File");
+
+ request_sender_->StartRequestWithRetry(request);
+ run_loop.Run();
+ }
+
+ EXPECT_EQ(HTTP_SUCCESS, result_code);
+ EXPECT_EQ(net::test_server::METHOD_PUT, http_request_.method);
+ EXPECT_EQ(
+ "/feeds/default/private/full/file%3A2_file_resource_id?v=3&alt=json"
+ "&showroot=true",
+ http_request_.relative_url);
+ EXPECT_EQ("application/atom+xml", http_request_.headers["Content-Type"]);
+ EXPECT_EQ("*", http_request_.headers["If-Match"]);
+
+ EXPECT_TRUE(http_request_.has_content);
+ EXPECT_EQ("<?xml version=\"1.0\"?>\n"
+ "<entry xmlns=\"http://www.w3.org/2005/Atom\">\n"
+ " <title>New File</title>\n"
+ "</entry>\n",
+ http_request_.content);
+}
+
+TEST_F(GDataWapiRequestsTest, AuthorizeAppRequest_ValidFeed) {
+ GDataErrorCode result_code = GDATA_OTHER_ERROR;
+ GURL result_data;
+
+ // Authorize an app with APP_ID to access to a document.
+ {
+ base::RunLoop run_loop;
+ AuthorizeAppRequest* request = new AuthorizeAppRequest(
+ request_sender_.get(),
+ *url_generator_,
+ test_util::CreateQuitCallback(
+ &run_loop,
+ test_util::CreateCopyResultCallback(&result_code, &result_data)),
+ "file:2_file_resource_id",
+ "the_app_id");
+
+ request_sender_->StartRequestWithRetry(request);
+ run_loop.Run();
+ }
+
+ EXPECT_EQ(HTTP_SUCCESS, result_code);
+ EXPECT_EQ(GURL("https://entry1_open_with_link/"), result_data);
+
+ EXPECT_EQ(net::test_server::METHOD_PUT, http_request_.method);
+ EXPECT_EQ("/feeds/default/private/full/file%3A2_file_resource_id"
+ "?v=3&alt=json&showroot=true",
+ http_request_.relative_url);
+ EXPECT_EQ("application/atom+xml", http_request_.headers["Content-Type"]);
+ EXPECT_EQ("*", http_request_.headers["If-Match"]);
+
+ EXPECT_TRUE(http_request_.has_content);
+ EXPECT_EQ("<?xml version=\"1.0\"?>\n"
+ "<entry xmlns=\"http://www.w3.org/2005/Atom\" "
+ "xmlns:docs=\"http://schemas.google.com/docs/2007\">\n"
+ " <docs:authorizedApp>the_app_id</docs:authorizedApp>\n"
+ "</entry>\n",
+ http_request_.content);
+}
+
+TEST_F(GDataWapiRequestsTest, AuthorizeAppRequest_NotFound) {
+ GDataErrorCode result_code = GDATA_OTHER_ERROR;
+ GURL result_data;
+
+ // Authorize an app with APP_ID to access to a document.
+ {
+ base::RunLoop run_loop;
+ AuthorizeAppRequest* request = new AuthorizeAppRequest(
+ request_sender_.get(),
+ *url_generator_,
+ test_util::CreateQuitCallback(
+ &run_loop,
+ test_util::CreateCopyResultCallback(&result_code, &result_data)),
+ "file:2_file_resource_id",
+ "unauthorized_app_id");
+
+ request_sender_->StartRequestWithRetry(request);
+ run_loop.Run();
+ }
+
+ EXPECT_EQ(GDATA_OTHER_ERROR, result_code);
+ EXPECT_EQ(net::test_server::METHOD_PUT, http_request_.method);
+ EXPECT_EQ("/feeds/default/private/full/file%3A2_file_resource_id"
+ "?v=3&alt=json&showroot=true",
+ http_request_.relative_url);
+ EXPECT_EQ("application/atom+xml", http_request_.headers["Content-Type"]);
+ EXPECT_EQ("*", http_request_.headers["If-Match"]);
+
+ EXPECT_TRUE(http_request_.has_content);
+ EXPECT_EQ("<?xml version=\"1.0\"?>\n"
+ "<entry xmlns=\"http://www.w3.org/2005/Atom\" "
+ "xmlns:docs=\"http://schemas.google.com/docs/2007\">\n"
+ " <docs:authorizedApp>unauthorized_app_id</docs:authorizedApp>\n"
+ "</entry>\n",
+ http_request_.content);
+}
+
+TEST_F(GDataWapiRequestsTest, AuthorizeAppRequest_InvalidFeed) {
+ GDataErrorCode result_code = GDATA_OTHER_ERROR;
+ GURL result_data;
+
+ // Authorize an app with APP_ID to access to a document but an invalid feed.
+ {
+ base::RunLoop run_loop;
+ AuthorizeAppRequest* request = new AuthorizeAppRequest(
+ request_sender_.get(),
+ *url_generator_,
+ test_util::CreateQuitCallback(
+ &run_loop,
+ test_util::CreateCopyResultCallback(&result_code, &result_data)),
+ "invalid_resource_id",
+ "APP_ID");
+
+ request_sender_->StartRequestWithRetry(request);
+ run_loop.Run();
+ }
+
+ EXPECT_EQ(GDATA_PARSE_ERROR, result_code);
+ EXPECT_EQ(net::test_server::METHOD_PUT, http_request_.method);
+ EXPECT_EQ("/feeds/default/private/full/invalid_resource_id"
+ "?v=3&alt=json&showroot=true",
+ http_request_.relative_url);
+ EXPECT_EQ("application/atom+xml", http_request_.headers["Content-Type"]);
+ EXPECT_EQ("*", http_request_.headers["If-Match"]);
+
+ EXPECT_TRUE(http_request_.has_content);
+ EXPECT_EQ("<?xml version=\"1.0\"?>\n"
+ "<entry xmlns=\"http://www.w3.org/2005/Atom\" "
+ "xmlns:docs=\"http://schemas.google.com/docs/2007\">\n"
+ " <docs:authorizedApp>APP_ID</docs:authorizedApp>\n"
+ "</entry>\n",
+ http_request_.content);
+}
+
+TEST_F(GDataWapiRequestsTest, AddResourceToDirectoryRequest) {
+ GDataErrorCode result_code = GDATA_OTHER_ERROR;
+
+ // Add a file to the root directory.
+ {
+ base::RunLoop run_loop;
+ AddResourceToDirectoryRequest* request =
+ new AddResourceToDirectoryRequest(
+ request_sender_.get(),
+ *url_generator_,
+ test_util::CreateQuitCallback(
+ &run_loop,
+ test_util::CreateCopyResultCallback(&result_code)),
+ "folder:root",
+ "file:2_file_resource_id");
+
+ request_sender_->StartRequestWithRetry(request);
+ run_loop.Run();
+ }
+
+ EXPECT_EQ(HTTP_SUCCESS, result_code);
+ EXPECT_EQ(net::test_server::METHOD_POST, http_request_.method);
+ EXPECT_EQ("/feeds/default/private/full/folder%3Aroot/contents?v=3&alt=json"
+ "&showroot=true",
+ http_request_.relative_url);
+ EXPECT_EQ("application/atom+xml", http_request_.headers["Content-Type"]);
+
+ EXPECT_TRUE(http_request_.has_content);
+ EXPECT_EQ(base::StringPrintf("<?xml version=\"1.0\"?>\n"
+ "<entry xmlns=\"http://www.w3.org/2005/Atom\">\n"
+ " <id>%sfeeds/default/private/full/"
+ "file%%3A2_file_resource_id</id>\n"
+ "</entry>\n",
+ test_server_.base_url().spec().c_str()),
+ http_request_.content);
+}
+
+TEST_F(GDataWapiRequestsTest, RemoveResourceFromDirectoryRequest) {
+ GDataErrorCode result_code = GDATA_OTHER_ERROR;
+
+ // Remove a file from the root directory.
+ {
+ base::RunLoop run_loop;
+ RemoveResourceFromDirectoryRequest* request =
+ new RemoveResourceFromDirectoryRequest(
+ request_sender_.get(),
+ *url_generator_,
+ test_util::CreateQuitCallback(
+ &run_loop,
+ test_util::CreateCopyResultCallback(&result_code)),
+ "folder:root",
+ "file:2_file_resource_id");
+
+ request_sender_->StartRequestWithRetry(request);
+ run_loop.Run();
+ }
+
+ EXPECT_EQ(HTTP_SUCCESS, result_code);
+ // DELETE method should be used, without the body content.
+ EXPECT_EQ(net::test_server::METHOD_DELETE, http_request_.method);
+ EXPECT_EQ("/feeds/default/private/full/folder%3Aroot/contents/"
+ "file%3A2_file_resource_id?v=3&alt=json&showroot=true",
+ http_request_.relative_url);
+ EXPECT_EQ("*", http_request_.headers["If-Match"]);
+ EXPECT_FALSE(http_request_.has_content);
+}
+
+// This test exercises InitiateUploadNewFileRequest and
+// ResumeUploadRequest for a scenario of uploading a new file.
+TEST_F(GDataWapiRequestsTest, UploadNewFile) {
+ const std::string kUploadContent = "hello";
+ const base::FilePath kTestFilePath =
+ temp_dir_.path().AppendASCII("upload_file.txt");
+ ASSERT_TRUE(test_util::WriteStringToFile(kTestFilePath, kUploadContent));
+
+ GDataErrorCode result_code = GDATA_OTHER_ERROR;
+ GURL upload_url;
+
+ // 1) Get the upload URL for uploading a new file.
+ {
+ base::RunLoop run_loop;
+ InitiateUploadNewFileRequest* initiate_request =
+ new InitiateUploadNewFileRequest(
+ request_sender_.get(),
+ *url_generator_,
+ test_util::CreateQuitCallback(
+ &run_loop,
+ test_util::CreateCopyResultCallback(&result_code, &upload_url)),
+ "text/plain",
+ kUploadContent.size(),
+ "folder:id",
+ "New file");
+ request_sender_->StartRequestWithRetry(initiate_request);
+ run_loop.Run();
+ }
+
+ EXPECT_EQ(HTTP_SUCCESS, result_code);
+ EXPECT_EQ(test_server_.GetURL("/upload_new_file"), upload_url);
+ EXPECT_EQ(net::test_server::METHOD_POST, http_request_.method);
+ // convert=false should be passed as files should be uploaded as-is.
+ EXPECT_EQ(
+ "/feeds/upload/create-session/default/private/full/folder%3Aid/contents"
+ "?convert=false&v=3&alt=json&showroot=true",
+ http_request_.relative_url);
+ EXPECT_EQ("text/plain", http_request_.headers["X-Upload-Content-Type"]);
+ EXPECT_EQ("application/atom+xml", http_request_.headers["Content-Type"]);
+ EXPECT_EQ(base::Int64ToString(kUploadContent.size()),
+ http_request_.headers["X-Upload-Content-Length"]);
+
+ EXPECT_TRUE(http_request_.has_content);
+ EXPECT_EQ("<?xml version=\"1.0\"?>\n"
+ "<entry xmlns=\"http://www.w3.org/2005/Atom\" "
+ "xmlns:docs=\"http://schemas.google.com/docs/2007\">\n"
+ " <title>New file</title>\n"
+ "</entry>\n",
+ http_request_.content);
+
+ // 2) Upload the content to the upload URL.
+ UploadRangeResponse response;
+ scoped_ptr<ResourceEntry> new_entry;
+
+ {
+ base::RunLoop run_loop;
+ ResumeUploadRequest* resume_request = new ResumeUploadRequest(
+ request_sender_.get(),
+ test_util::CreateQuitCallback(
+ &run_loop,
+ test_util::CreateCopyResultCallback(&response, &new_entry)),
+ ProgressCallback(),
+ upload_url,
+ 0, // start_position
+ kUploadContent.size(), // end_position (exclusive)
+ kUploadContent.size(), // content_length,
+ "text/plain", // content_type
+ kTestFilePath);
+
+ request_sender_->StartRequestWithRetry(resume_request);
+ run_loop.Run();
+ }
+
+ // METHOD_PUT should be used to upload data.
+ EXPECT_EQ(net::test_server::METHOD_PUT, http_request_.method);
+ // Request should go to the upload URL.
+ EXPECT_EQ(upload_url.path(), http_request_.relative_url);
+ // Content-Range header should be added.
+ EXPECT_EQ("bytes 0-" +
+ base::Int64ToString(kUploadContent.size() -1) + "/" +
+ base::Int64ToString(kUploadContent.size()),
+ http_request_.headers["Content-Range"]);
+ // The upload content should be set in the HTTP request.
+ EXPECT_TRUE(http_request_.has_content);
+ EXPECT_EQ(kUploadContent, http_request_.content);
+
+ // Check the response.
+ EXPECT_EQ(HTTP_CREATED, response.code); // Because it's a new file
+ // The start and end positions should be set to -1, if an upload is complete.
+ EXPECT_EQ(-1, response.start_position_received);
+ EXPECT_EQ(-1, response.end_position_received);
+}
+
+// This test exercises InitiateUploadNewFileRequest and ResumeUploadRequest
+// for a scenario of uploading a new *large* file, which requires multiple
+// requests of ResumeUploadRequest. GetUploadRequest is also tested in this
+// test case.
+TEST_F(GDataWapiRequestsTest, UploadNewLargeFile) {
+ const size_t kMaxNumBytes = 10;
+ // This is big enough to cause multiple requests of ResumeUploadRequest
+ // as we are going to send at most kMaxNumBytes at a time.
+ // So, sending "kMaxNumBytes * 2 + 1" bytes ensures three
+ // ResumeUploadRequests, which are start, middle and last requests.
+ const std::string kUploadContent(kMaxNumBytes * 2 + 1, 'a');
+ const base::FilePath kTestFilePath =
+ temp_dir_.path().AppendASCII("upload_file.txt");
+ ASSERT_TRUE(test_util::WriteStringToFile(kTestFilePath, kUploadContent));
+
+ GDataErrorCode result_code = GDATA_OTHER_ERROR;
+ GURL upload_url;
+
+ // 1) Get the upload URL for uploading a new file.
+ {
+ base::RunLoop run_loop;
+ InitiateUploadNewFileRequest* initiate_request =
+ new InitiateUploadNewFileRequest(
+ request_sender_.get(),
+ *url_generator_,
+ test_util::CreateQuitCallback(
+ &run_loop,
+ test_util::CreateCopyResultCallback(&result_code, &upload_url)),
+ "text/plain",
+ kUploadContent.size(),
+ "folder:id",
+ "New file");
+ request_sender_->StartRequestWithRetry(initiate_request);
+ run_loop.Run();
+ }
+
+ EXPECT_EQ(HTTP_SUCCESS, result_code);
+ EXPECT_EQ(test_server_.GetURL("/upload_new_file"), upload_url);
+ EXPECT_EQ(net::test_server::METHOD_POST, http_request_.method);
+ // convert=false should be passed as files should be uploaded as-is.
+ EXPECT_EQ(
+ "/feeds/upload/create-session/default/private/full/folder%3Aid/contents"
+ "?convert=false&v=3&alt=json&showroot=true",
+ http_request_.relative_url);
+ EXPECT_EQ("text/plain", http_request_.headers["X-Upload-Content-Type"]);
+ EXPECT_EQ("application/atom+xml", http_request_.headers["Content-Type"]);
+ EXPECT_EQ(base::Int64ToString(kUploadContent.size()),
+ http_request_.headers["X-Upload-Content-Length"]);
+
+ EXPECT_TRUE(http_request_.has_content);
+ EXPECT_EQ("<?xml version=\"1.0\"?>\n"
+ "<entry xmlns=\"http://www.w3.org/2005/Atom\" "
+ "xmlns:docs=\"http://schemas.google.com/docs/2007\">\n"
+ " <title>New file</title>\n"
+ "</entry>\n",
+ http_request_.content);
+
+ // 2) Before sending any data, check the current status.
+ // This is an edge case test for GetUploadStatusRequest
+ // (UploadRangeRequestBase).
+ {
+ UploadRangeResponse response;
+ scoped_ptr<ResourceEntry> new_entry;
+
+ // Check the response by GetUploadStatusRequest.
+ {
+ base::RunLoop run_loop;
+ GetUploadStatusRequest* get_upload_status_request =
+ new GetUploadStatusRequest(
+ request_sender_.get(),
+ test_util::CreateQuitCallback(
+ &run_loop,
+ test_util::CreateCopyResultCallback(&response, &new_entry)),
+ upload_url,
+ kUploadContent.size());
+ request_sender_->StartRequestWithRetry(get_upload_status_request);
+ run_loop.Run();
+ }
+
+ // METHOD_PUT should be used to upload data.
+ EXPECT_EQ(net::test_server::METHOD_PUT, http_request_.method);
+ // Request should go to the upload URL.
+ EXPECT_EQ(upload_url.path(), http_request_.relative_url);
+ // Content-Range header should be added.
+ EXPECT_EQ("bytes */" + base::Int64ToString(kUploadContent.size()),
+ http_request_.headers["Content-Range"]);
+ EXPECT_TRUE(http_request_.has_content);
+ EXPECT_TRUE(http_request_.content.empty());
+
+ // Check the response.
+ EXPECT_EQ(HTTP_RESUME_INCOMPLETE, response.code);
+ EXPECT_EQ(0, response.start_position_received);
+ EXPECT_EQ(0, response.end_position_received);
+ }
+
+ // 3) Upload the content to the upload URL with multiple requests.
+ size_t num_bytes_consumed = 0;
+ for (size_t start_position = 0; start_position < kUploadContent.size();
+ start_position += kMaxNumBytes) {
+ SCOPED_TRACE(testing::Message("start_position: ") << start_position);
+
+ // The payload is at most kMaxNumBytes.
+ const size_t remaining_size = kUploadContent.size() - start_position;
+ const std::string payload = kUploadContent.substr(
+ start_position, std::min(kMaxNumBytes, remaining_size));
+ num_bytes_consumed += payload.size();
+ // The end position is exclusive.
+ const size_t end_position = start_position + payload.size();
+
+ UploadRangeResponse response;
+ scoped_ptr<ResourceEntry> new_entry;
+
+ {
+ base::RunLoop run_loop;
+ ResumeUploadRequest* resume_request = new ResumeUploadRequest(
+ request_sender_.get(),
+ test_util::CreateQuitCallback(
+ &run_loop,
+ test_util::CreateCopyResultCallback(&response, &new_entry)),
+ ProgressCallback(),
+ upload_url,
+ start_position,
+ end_position,
+ kUploadContent.size(), // content_length,
+ "text/plain", // content_type
+ kTestFilePath);
+ request_sender_->StartRequestWithRetry(resume_request);
+ run_loop.Run();
+ }
+
+ // METHOD_PUT should be used to upload data.
+ EXPECT_EQ(net::test_server::METHOD_PUT, http_request_.method);
+ // Request should go to the upload URL.
+ EXPECT_EQ(upload_url.path(), http_request_.relative_url);
+ // Content-Range header should be added.
+ EXPECT_EQ("bytes " +
+ base::Int64ToString(start_position) + "-" +
+ base::Int64ToString(end_position - 1) + "/" +
+ base::Int64ToString(kUploadContent.size()),
+ http_request_.headers["Content-Range"]);
+ // The upload content should be set in the HTTP request.
+ EXPECT_TRUE(http_request_.has_content);
+ EXPECT_EQ(payload, http_request_.content);
+
+ // Check the response.
+ if (payload.size() == remaining_size) {
+ EXPECT_EQ(HTTP_CREATED, response.code); // Because it's a new file.
+ // The start and end positions should be set to -1, if an upload is
+ // complete.
+ EXPECT_EQ(-1, response.start_position_received);
+ EXPECT_EQ(-1, response.end_position_received);
+ // The upload process is completed, so exit from the loop.
+ break;
+ }
+
+ EXPECT_EQ(HTTP_RESUME_INCOMPLETE, response.code);
+ EXPECT_EQ(0, response.start_position_received);
+ EXPECT_EQ(static_cast<int64>(end_position),
+ response.end_position_received);
+
+ // Check the response by GetUploadStatusRequest.
+ {
+ base::RunLoop run_loop;
+ GetUploadStatusRequest* get_upload_status_request =
+ new GetUploadStatusRequest(
+ request_sender_.get(),
+ test_util::CreateQuitCallback(
+ &run_loop,
+ test_util::CreateCopyResultCallback(&response, &new_entry)),
+ upload_url,
+ kUploadContent.size());
+ request_sender_->StartRequestWithRetry(get_upload_status_request);
+ run_loop.Run();
+ }
+
+ // METHOD_PUT should be used to upload data.
+ EXPECT_EQ(net::test_server::METHOD_PUT, http_request_.method);
+ // Request should go to the upload URL.
+ EXPECT_EQ(upload_url.path(), http_request_.relative_url);
+ // Content-Range header should be added.
+ EXPECT_EQ("bytes */" + base::Int64ToString(kUploadContent.size()),
+ http_request_.headers["Content-Range"]);
+ EXPECT_TRUE(http_request_.has_content);
+ EXPECT_TRUE(http_request_.content.empty());
+
+ // Check the response.
+ EXPECT_EQ(HTTP_RESUME_INCOMPLETE, response.code);
+ EXPECT_EQ(0, response.start_position_received);
+ EXPECT_EQ(static_cast<int64>(end_position),
+ response.end_position_received);
+ }
+
+ EXPECT_EQ(kUploadContent.size(), num_bytes_consumed);
+}
+
+// This test exercises InitiateUploadNewFileRequest and ResumeUploadRequest
+// for a scenario of uploading a new *empty* file.
+//
+// The test is almost identical to UploadNewFile. The only difference is the
+// expectation for the Content-Range header.
+TEST_F(GDataWapiRequestsTest, UploadNewEmptyFile) {
+ const std::string kUploadContent;
+ const base::FilePath kTestFilePath =
+ temp_dir_.path().AppendASCII("empty_file.txt");
+ ASSERT_TRUE(test_util::WriteStringToFile(kTestFilePath, kUploadContent));
+
+ GDataErrorCode result_code = GDATA_OTHER_ERROR;
+ GURL upload_url;
+
+ // 1) Get the upload URL for uploading a new file.
+ {
+ base::RunLoop run_loop;
+ InitiateUploadNewFileRequest* initiate_request =
+ new InitiateUploadNewFileRequest(
+ request_sender_.get(),
+ *url_generator_,
+ test_util::CreateQuitCallback(
+ &run_loop,
+ test_util::CreateCopyResultCallback(&result_code, &upload_url)),
+ "text/plain",
+ kUploadContent.size(),
+ "folder:id",
+ "New file");
+ request_sender_->StartRequestWithRetry(initiate_request);
+ run_loop.Run();
+ }
+
+ EXPECT_EQ(HTTP_SUCCESS, result_code);
+ EXPECT_EQ(test_server_.GetURL("/upload_new_file"), upload_url);
+ EXPECT_EQ(net::test_server::METHOD_POST, http_request_.method);
+ // convert=false should be passed as files should be uploaded as-is.
+ EXPECT_EQ(
+ "/feeds/upload/create-session/default/private/full/folder%3Aid/contents"
+ "?convert=false&v=3&alt=json&showroot=true",
+ http_request_.relative_url);
+ EXPECT_EQ("text/plain", http_request_.headers["X-Upload-Content-Type"]);
+ EXPECT_EQ("application/atom+xml", http_request_.headers["Content-Type"]);
+ EXPECT_EQ(base::Int64ToString(kUploadContent.size()),
+ http_request_.headers["X-Upload-Content-Length"]);
+
+ EXPECT_TRUE(http_request_.has_content);
+ EXPECT_EQ("<?xml version=\"1.0\"?>\n"
+ "<entry xmlns=\"http://www.w3.org/2005/Atom\" "
+ "xmlns:docs=\"http://schemas.google.com/docs/2007\">\n"
+ " <title>New file</title>\n"
+ "</entry>\n",
+ http_request_.content);
+
+ // 2) Upload the content to the upload URL.
+ UploadRangeResponse response;
+ scoped_ptr<ResourceEntry> new_entry;
+
+ {
+ base::RunLoop run_loop;
+ ResumeUploadRequest* resume_request = new ResumeUploadRequest(
+ request_sender_.get(),
+ test_util::CreateQuitCallback(
+ &run_loop,
+ test_util::CreateCopyResultCallback(&response, &new_entry)),
+ ProgressCallback(),
+ upload_url,
+ 0, // start_position
+ kUploadContent.size(), // end_position (exclusive)
+ kUploadContent.size(), // content_length,
+ "text/plain", // content_type
+ kTestFilePath);
+ request_sender_->StartRequestWithRetry(resume_request);
+ run_loop.Run();
+ }
+
+ // METHOD_PUT should be used to upload data.
+ EXPECT_EQ(net::test_server::METHOD_PUT, http_request_.method);
+ // Request should go to the upload URL.
+ EXPECT_EQ(upload_url.path(), http_request_.relative_url);
+ // Content-Range header should not exit if the content is empty.
+ // We should not generate the header with an invalid value "bytes 0--1/0".
+ EXPECT_EQ(0U, http_request_.headers.count("Content-Range"));
+ // The upload content should be set in the HTTP request.
+ EXPECT_TRUE(http_request_.has_content);
+ EXPECT_EQ(kUploadContent, http_request_.content);
+
+ // Check the response.
+ EXPECT_EQ(HTTP_CREATED, response.code); // Because it's a new file.
+ // The start and end positions should be set to -1, if an upload is complete.
+ EXPECT_EQ(-1, response.start_position_received);
+ EXPECT_EQ(-1, response.end_position_received);
+}
+
+// This test exercises InitiateUploadExistingFileRequest and
+// ResumeUploadRequest for a scenario of updating an existing file.
+TEST_F(GDataWapiRequestsTest, UploadExistingFile) {
+ const std::string kUploadContent = "hello";
+ const base::FilePath kTestFilePath =
+ temp_dir_.path().AppendASCII("upload_file.txt");
+ ASSERT_TRUE(test_util::WriteStringToFile(kTestFilePath, kUploadContent));
+
+ GDataErrorCode result_code = GDATA_OTHER_ERROR;
+ GURL upload_url;
+
+ // 1) Get the upload URL for uploading an existing file.
+ {
+ base::RunLoop run_loop;
+ InitiateUploadExistingFileRequest* initiate_request =
+ new InitiateUploadExistingFileRequest(
+ request_sender_.get(),
+ *url_generator_,
+ test_util::CreateQuitCallback(
+ &run_loop,
+ test_util::CreateCopyResultCallback(&result_code, &upload_url)),
+ "text/plain",
+ kUploadContent.size(),
+ "file:foo",
+ std::string() /* etag */);
+ request_sender_->StartRequestWithRetry(initiate_request);
+ run_loop.Run();
+ }
+
+ EXPECT_EQ(HTTP_SUCCESS, result_code);
+ EXPECT_EQ(test_server_.GetURL("/upload_existing_file"), upload_url);
+ // For updating an existing file, METHOD_PUT should be used.
+ EXPECT_EQ(net::test_server::METHOD_PUT, http_request_.method);
+ // convert=false should be passed as files should be uploaded as-is.
+ EXPECT_EQ("/feeds/upload/create-session/default/private/full/file%3Afoo"
+ "?convert=false&v=3&alt=json&showroot=true",
+ http_request_.relative_url);
+ // Even though the body is empty, the content type should be set to
+ // "text/plain".
+ EXPECT_EQ("text/plain", http_request_.headers["Content-Type"]);
+ EXPECT_EQ("text/plain", http_request_.headers["X-Upload-Content-Type"]);
+ EXPECT_EQ(base::Int64ToString(kUploadContent.size()),
+ http_request_.headers["X-Upload-Content-Length"]);
+ // For updating an existing file, an empty body should be attached (PUT
+ // requires a body)
+ EXPECT_TRUE(http_request_.has_content);
+ EXPECT_EQ("", http_request_.content);
+ EXPECT_EQ("*", http_request_.headers["If-Match"]);
+
+ // 2) Upload the content to the upload URL.
+ UploadRangeResponse response;
+ scoped_ptr<ResourceEntry> new_entry;
+
+ {
+ base::RunLoop run_loop;
+ ResumeUploadRequest* resume_request = new ResumeUploadRequest(
+ request_sender_.get(),
+ test_util::CreateQuitCallback(
+ &run_loop,
+ test_util::CreateCopyResultCallback(&response, &new_entry)),
+ ProgressCallback(),
+ upload_url,
+ 0, // start_position
+ kUploadContent.size(), // end_position (exclusive)
+ kUploadContent.size(), // content_length,
+ "text/plain", // content_type
+ kTestFilePath);
+
+ request_sender_->StartRequestWithRetry(resume_request);
+ run_loop.Run();
+ }
+
+ // METHOD_PUT should be used to upload data.
+ EXPECT_EQ(net::test_server::METHOD_PUT, http_request_.method);
+ // Request should go to the upload URL.
+ EXPECT_EQ(upload_url.path(), http_request_.relative_url);
+ // Content-Range header should be added.
+ EXPECT_EQ("bytes 0-" +
+ base::Int64ToString(kUploadContent.size() -1) + "/" +
+ base::Int64ToString(kUploadContent.size()),
+ http_request_.headers["Content-Range"]);
+ // The upload content should be set in the HTTP request.
+ EXPECT_TRUE(http_request_.has_content);
+ EXPECT_EQ(kUploadContent, http_request_.content);
+
+ // Check the response.
+ EXPECT_EQ(HTTP_SUCCESS, response.code); // Because it's an existing file.
+ // The start and end positions should be set to -1, if an upload is complete.
+ EXPECT_EQ(-1, response.start_position_received);
+ EXPECT_EQ(-1, response.end_position_received);
+}
+
+// This test exercises InitiateUploadExistingFileRequest and
+// ResumeUploadRequest for a scenario of updating an existing file.
+TEST_F(GDataWapiRequestsTest, UploadExistingFileWithETag) {
+ const std::string kUploadContent = "hello";
+ const base::FilePath kTestFilePath =
+ temp_dir_.path().AppendASCII("upload_file.txt");
+ ASSERT_TRUE(test_util::WriteStringToFile(kTestFilePath, kUploadContent));
+
+ GDataErrorCode result_code = GDATA_OTHER_ERROR;
+ GURL upload_url;
+
+ // 1) Get the upload URL for uploading an existing file.
+ {
+ base::RunLoop run_loop;
+ InitiateUploadExistingFileRequest* initiate_request =
+ new InitiateUploadExistingFileRequest(
+ request_sender_.get(),
+ *url_generator_,
+ test_util::CreateQuitCallback(
+ &run_loop,
+ test_util::CreateCopyResultCallback(&result_code, &upload_url)),
+ "text/plain",
+ kUploadContent.size(),
+ "file:foo",
+ kTestETag);
+ request_sender_->StartRequestWithRetry(initiate_request);
+ run_loop.Run();
+ }
+
+ EXPECT_EQ(HTTP_SUCCESS, result_code);
+ EXPECT_EQ(test_server_.GetURL("/upload_existing_file"), upload_url);
+ // For updating an existing file, METHOD_PUT should be used.
+ EXPECT_EQ(net::test_server::METHOD_PUT, http_request_.method);
+ // convert=false should be passed as files should be uploaded as-is.
+ EXPECT_EQ("/feeds/upload/create-session/default/private/full/file%3Afoo"
+ "?convert=false&v=3&alt=json&showroot=true",
+ http_request_.relative_url);
+ // Even though the body is empty, the content type should be set to
+ // "text/plain".
+ EXPECT_EQ("text/plain", http_request_.headers["Content-Type"]);
+ EXPECT_EQ("text/plain", http_request_.headers["X-Upload-Content-Type"]);
+ EXPECT_EQ(base::Int64ToString(kUploadContent.size()),
+ http_request_.headers["X-Upload-Content-Length"]);
+ // For updating an existing file, an empty body should be attached (PUT
+ // requires a body)
+ EXPECT_TRUE(http_request_.has_content);
+ EXPECT_EQ("", http_request_.content);
+ EXPECT_EQ(kTestETag, http_request_.headers["If-Match"]);
+
+ // 2) Upload the content to the upload URL.
+ UploadRangeResponse response;
+ scoped_ptr<ResourceEntry> new_entry;
+
+ {
+ base::RunLoop run_loop;
+ ResumeUploadRequest* resume_request = new ResumeUploadRequest(
+ request_sender_.get(),
+ test_util::CreateQuitCallback(
+ &run_loop,
+ test_util::CreateCopyResultCallback(&response, &new_entry)),
+ ProgressCallback(),
+ upload_url,
+ 0, // start_position
+ kUploadContent.size(), // end_position (exclusive)
+ kUploadContent.size(), // content_length,
+ "text/plain", // content_type
+ kTestFilePath);
+ request_sender_->StartRequestWithRetry(resume_request);
+ run_loop.Run();
+ }
+
+ // METHOD_PUT should be used to upload data.
+ EXPECT_EQ(net::test_server::METHOD_PUT, http_request_.method);
+ // Request should go to the upload URL.
+ EXPECT_EQ(upload_url.path(), http_request_.relative_url);
+ // Content-Range header should be added.
+ EXPECT_EQ("bytes 0-" +
+ base::Int64ToString(kUploadContent.size() -1) + "/" +
+ base::Int64ToString(kUploadContent.size()),
+ http_request_.headers["Content-Range"]);
+ // The upload content should be set in the HTTP request.
+ EXPECT_TRUE(http_request_.has_content);
+ EXPECT_EQ(kUploadContent, http_request_.content);
+
+ // Check the response.
+ EXPECT_EQ(HTTP_SUCCESS, response.code); // Because it's an existing file.
+ // The start and end positions should be set to -1, if an upload is complete.
+ EXPECT_EQ(-1, response.start_position_received);
+ EXPECT_EQ(-1, response.end_position_received);
+}
+
+// This test exercises InitiateUploadExistingFileRequest for a scenario of
+// confliction on updating an existing file.
+TEST_F(GDataWapiRequestsTest, UploadExistingFileWithETagConflict) {
+ const std::string kUploadContent = "hello";
+ const std::string kWrongETag = "wrong_etag";
+ GDataErrorCode result_code = GDATA_OTHER_ERROR;
+ GURL upload_url;
+
+ {
+ base::RunLoop run_loop;
+ InitiateUploadExistingFileRequest* initiate_request =
+ new InitiateUploadExistingFileRequest(
+ request_sender_.get(),
+ *url_generator_,
+ test_util::CreateQuitCallback(
+ &run_loop,
+ test_util::CreateCopyResultCallback(&result_code, &upload_url)),
+ "text/plain",
+ kUploadContent.size(),
+ "file:foo",
+ kWrongETag);
+ request_sender_->StartRequestWithRetry(initiate_request);
+ run_loop.Run();
+ }
+
+ EXPECT_EQ(HTTP_PRECONDITION, result_code);
+ // For updating an existing file, METHOD_PUT should be used.
+ EXPECT_EQ(net::test_server::METHOD_PUT, http_request_.method);
+ // convert=false should be passed as files should be uploaded as-is.
+ EXPECT_EQ("/feeds/upload/create-session/default/private/full/file%3Afoo"
+ "?convert=false&v=3&alt=json&showroot=true",
+ http_request_.relative_url);
+ // Even though the body is empty, the content type should be set to
+ // "text/plain".
+ EXPECT_EQ("text/plain", http_request_.headers["Content-Type"]);
+ EXPECT_EQ("text/plain", http_request_.headers["X-Upload-Content-Type"]);
+ EXPECT_EQ(base::Int64ToString(kUploadContent.size()),
+ http_request_.headers["X-Upload-Content-Length"]);
+ // For updating an existing file, an empty body should be attached (PUT
+ // requires a body)
+ EXPECT_TRUE(http_request_.has_content);
+ EXPECT_EQ("", http_request_.content);
+ EXPECT_EQ(kWrongETag, http_request_.headers["If-Match"]);
+}
+
+TEST_F(GDataWapiRequestsTest, DownloadFileRequest) {
+ const base::FilePath kDownloadedFilePath =
+ temp_dir_.path().AppendASCII("cache_file");
+ const std::string kTestIdWithTypeLabel("file:dummyId");
+ const std::string kTestId("dummyId");
+
+ GDataErrorCode result_code = GDATA_OTHER_ERROR;
+ base::FilePath temp_file;
+ {
+ base::RunLoop run_loop;
+ DownloadFileRequest* request = new DownloadFileRequest(
+ request_sender_.get(),
+ *url_generator_,
+ test_util::CreateQuitCallback(
+ &run_loop,
+ test_util::CreateCopyResultCallback(&result_code, &temp_file)),
+ GetContentCallback(),
+ ProgressCallback(),
+ kTestIdWithTypeLabel,
+ kDownloadedFilePath);
+ request_sender_->StartRequestWithRetry(request);
+ run_loop.Run();
+ }
+
+ std::string contents;
+ base::ReadFileToString(temp_file, &contents);
+ base::DeleteFile(temp_file, false);
+
+ EXPECT_EQ(HTTP_SUCCESS, result_code);
+ EXPECT_EQ(net::test_server::METHOD_GET, http_request_.method);
+ EXPECT_EQ(kTestDownloadPathPrefix + kTestId, http_request_.relative_url);
+ EXPECT_EQ(kDownloadedFilePath, temp_file);
+
+ const std::string expected_contents = kTestId + kTestId + kTestId;
+ EXPECT_EQ(expected_contents, contents);
+}
+
+} // namespace google_apis
diff --git a/chromium/google_apis/drive/gdata_wapi_url_generator.cc b/chromium/google_apis/drive/gdata_wapi_url_generator.cc
new file mode 100644
index 00000000000..c1263b71f34
--- /dev/null
+++ b/chromium/google_apis/drive/gdata_wapi_url_generator.cc
@@ -0,0 +1,250 @@
+// Copyright (c) 2012 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "google_apis/drive/gdata_wapi_url_generator.h"
+
+#include "base/logging.h"
+#include "base/strings/string_number_conversions.h"
+#include "base/strings/stringprintf.h"
+#include "net/base/escape.h"
+#include "net/base/url_util.h"
+#include "url/gurl.h"
+
+namespace google_apis {
+namespace {
+
+// Content URL for modification or resource list retrieval in a particular
+// directory specified by "%s" which will be replaced with its resource id.
+const char kContentURLFormat[] = "/feeds/default/private/full/%s/contents";
+
+// Content URL for removing a resource specified by the latter "%s" from the
+// directory specified by the former "%s".
+const char kResourceURLForRemovalFormat[] =
+ "/feeds/default/private/full/%s/contents/%s";
+
+// URL requesting single resource entry whose resource id is followed by this
+// prefix.
+const char kGetEditURLPrefix[] = "/feeds/default/private/full/";
+
+// Root resource list url.
+const char kResourceListRootURL[] = "/feeds/default/private/full";
+
+// Metadata feed with things like user quota.
+const char kAccountMetadataURL[] = "/feeds/metadata/default";
+
+// URL to upload a new file under a particular directory specified by "%s".
+const char kInitiateUploadNewFileURLFormat[] =
+ "/feeds/upload/create-session/default/private/full/%s/contents";
+
+// URL to upload a file content to overwrite a file whose resource id is
+// followed by this prefix.
+const char kInitiateUploadExistingFileURLPrefix[] =
+ "/feeds/upload/create-session/default/private/full/";
+
+// Maximum number of resource entries to include in a feed.
+// Be careful not to use something too small because it might overload the
+// server. Be careful not to use something too large because it makes the
+// "fetched N items" UI less responsive.
+const int kMaxDocumentsPerFeed = 500;
+const int kMaxDocumentsPerSearchFeed = 50;
+
+// URL requesting documents list of changes to documents collections.
+const char kGetChangesListURL[] = "/feeds/default/private/changes";
+
+} // namespace
+
+const char GDataWapiUrlGenerator::kBaseUrlForProduction[] =
+ "https://docs.google.com/";
+
+const char GDataWapiUrlGenerator::kBaseDownloadUrlForProduction[] =
+ "https://www.googledrive.com/host/";
+
+// static
+GURL GDataWapiUrlGenerator::AddStandardUrlParams(const GURL& url) {
+ GURL result = net::AppendOrReplaceQueryParameter(url, "v", "3");
+ result = net::AppendOrReplaceQueryParameter(result, "alt", "json");
+ result = net::AppendOrReplaceQueryParameter(result, "showroot", "true");
+ return result;
+}
+
+// static
+GURL GDataWapiUrlGenerator::AddInitiateUploadUrlParams(const GURL& url) {
+ GURL result = net::AppendOrReplaceQueryParameter(url, "convert", "false");
+ return AddStandardUrlParams(result);
+}
+
+// static
+GURL GDataWapiUrlGenerator::AddFeedUrlParams(
+ const GURL& url,
+ int num_items_to_fetch) {
+ GURL result = AddStandardUrlParams(url);
+ result = net::AppendOrReplaceQueryParameter(result, "showfolders", "true");
+ result = net::AppendOrReplaceQueryParameter(result, "include-shared", "true");
+ result = net::AppendOrReplaceQueryParameter(
+ result, "max-results", base::IntToString(num_items_to_fetch));
+ return result;
+}
+
+GDataWapiUrlGenerator::GDataWapiUrlGenerator(const GURL& base_url,
+ const GURL& base_download_url)
+ : base_url_(base_url),
+ base_download_url_(base_download_url) {
+}
+
+GDataWapiUrlGenerator::~GDataWapiUrlGenerator() {
+}
+
+GURL GDataWapiUrlGenerator::GenerateResourceListUrl(
+ const GURL& override_url,
+ int64 start_changestamp,
+ const std::string& search_string,
+ const std::string& directory_resource_id) const {
+ DCHECK_LE(0, start_changestamp);
+
+ int max_docs = search_string.empty() ? kMaxDocumentsPerFeed :
+ kMaxDocumentsPerSearchFeed;
+ GURL url;
+ if (!override_url.is_empty()) {
+ // |override_url| specifies the URL of the continuation feed when the feed
+ // is broken up to multiple chunks. In this case we must not add the
+ // |start_changestamp| that provides the original start point.
+ start_changestamp = 0;
+ url = override_url;
+ } else if (start_changestamp > 0) {
+ // The start changestamp shouldn't be used for a search.
+ DCHECK(search_string.empty());
+ url = base_url_.Resolve(kGetChangesListURL);
+ } else if (!directory_resource_id.empty()) {
+ url = base_url_.Resolve(
+ base::StringPrintf(kContentURLFormat,
+ net::EscapePath(
+ directory_resource_id).c_str()));
+ } else {
+ url = base_url_.Resolve(kResourceListRootURL);
+ }
+
+ url = AddFeedUrlParams(url, max_docs);
+
+ if (start_changestamp) {
+ url = net::AppendOrReplaceQueryParameter(
+ url, "start-index", base::Int64ToString(start_changestamp));
+ }
+ if (!search_string.empty()) {
+ url = net::AppendOrReplaceQueryParameter(url, "q", search_string);
+ }
+
+ return url;
+}
+
+GURL GDataWapiUrlGenerator::GenerateSearchByTitleUrl(
+ const std::string& title,
+ const std::string& directory_resource_id) const {
+ DCHECK(!title.empty());
+
+ GURL url = directory_resource_id.empty() ?
+ base_url_.Resolve(kResourceListRootURL) :
+ base_url_.Resolve(base::StringPrintf(
+ kContentURLFormat, net::EscapePath(directory_resource_id).c_str()));
+ url = AddFeedUrlParams(url, kMaxDocumentsPerFeed);
+ url = net::AppendOrReplaceQueryParameter(url, "title", title);
+ url = net::AppendOrReplaceQueryParameter(url, "title-exact", "true");
+ return url;
+}
+
+GURL GDataWapiUrlGenerator::GenerateEditUrl(
+ const std::string& resource_id) const {
+ return AddStandardUrlParams(GenerateEditUrlWithoutParams(resource_id));
+}
+
+GURL GDataWapiUrlGenerator::GenerateEditUrlWithoutParams(
+ const std::string& resource_id) const {
+ return base_url_.Resolve(kGetEditURLPrefix + net::EscapePath(resource_id));
+}
+
+GURL GDataWapiUrlGenerator::GenerateEditUrlWithEmbedOrigin(
+ const std::string& resource_id, const GURL& embed_origin) const {
+ GURL url = GenerateEditUrl(resource_id);
+ if (!embed_origin.is_empty()) {
+ // Construct a valid serialized embed origin from an url, according to
+ // WD-html5-20110525. Such string has to be built manually, since
+ // GURL::spec() always adds the trailing slash. Moreover, ports are
+ // currently not supported.
+ DCHECK(!embed_origin.has_port());
+ DCHECK(!embed_origin.has_path() || embed_origin.path() == "/");
+ const std::string serialized_embed_origin =
+ embed_origin.scheme() + "://" + embed_origin.host();
+ url = net::AppendOrReplaceQueryParameter(
+ url, "embedOrigin", serialized_embed_origin);
+ }
+ return url;
+}
+
+GURL GDataWapiUrlGenerator::GenerateContentUrl(
+ const std::string& resource_id) const {
+ if (resource_id.empty()) {
+ // |resource_id| must not be empty. Return an empty GURL as an error.
+ return GURL();
+ }
+
+ GURL result = base_url_.Resolve(
+ base::StringPrintf(kContentURLFormat,
+ net::EscapePath(resource_id).c_str()));
+ return AddStandardUrlParams(result);
+}
+
+GURL GDataWapiUrlGenerator::GenerateResourceUrlForRemoval(
+ const std::string& parent_resource_id,
+ const std::string& resource_id) const {
+ if (resource_id.empty() || parent_resource_id.empty()) {
+ // Both |resource_id| and |parent_resource_id| must be non-empty.
+ // Return an empty GURL as an error.
+ return GURL();
+ }
+
+ GURL result = base_url_.Resolve(
+ base::StringPrintf(kResourceURLForRemovalFormat,
+ net::EscapePath(parent_resource_id).c_str(),
+ net::EscapePath(resource_id).c_str()));
+ return AddStandardUrlParams(result);
+}
+
+GURL GDataWapiUrlGenerator::GenerateInitiateUploadNewFileUrl(
+ const std::string& parent_resource_id) const {
+ GURL result = base_url_.Resolve(
+ base::StringPrintf(kInitiateUploadNewFileURLFormat,
+ net::EscapePath(parent_resource_id).c_str()));
+ return AddInitiateUploadUrlParams(result);
+}
+
+GURL GDataWapiUrlGenerator::GenerateInitiateUploadExistingFileUrl(
+ const std::string& resource_id) const {
+ GURL result = base_url_.Resolve(
+ kInitiateUploadExistingFileURLPrefix + net::EscapePath(resource_id));
+ return AddInitiateUploadUrlParams(result);
+}
+
+GURL GDataWapiUrlGenerator::GenerateResourceListRootUrl() const {
+ return AddStandardUrlParams(base_url_.Resolve(kResourceListRootURL));
+}
+
+GURL GDataWapiUrlGenerator::GenerateAccountMetadataUrl(
+ bool include_installed_apps) const {
+ GURL result = AddStandardUrlParams(base_url_.Resolve(kAccountMetadataURL));
+ if (include_installed_apps) {
+ result = net::AppendOrReplaceQueryParameter(
+ result, "include-installed-apps", "true");
+ }
+ return result;
+}
+
+GURL GDataWapiUrlGenerator::GenerateDownloadFileUrl(
+ const std::string& resource_id) const {
+ // Strip the file type prefix before the colon character.
+ size_t colon = resource_id.find(':');
+ return base_download_url_.Resolve(net::EscapePath(
+ colon == std::string::npos ? resource_id
+ : resource_id.substr(colon + 1)));
+}
+
+} // namespace google_apis
diff --git a/chromium/google_apis/drive/gdata_wapi_url_generator.h b/chromium/google_apis/drive/gdata_wapi_url_generator.h
new file mode 100644
index 00000000000..05b565d404b
--- /dev/null
+++ b/chromium/google_apis/drive/gdata_wapi_url_generator.h
@@ -0,0 +1,140 @@
+// Copyright (c) 2012 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+//
+// URL utility functions for Google Documents List API (aka WAPI).
+
+#ifndef GOOGLE_APIS_DRIVE_GDATA_WAPI_URL_GENERATOR_H_
+#define GOOGLE_APIS_DRIVE_GDATA_WAPI_URL_GENERATOR_H_
+
+#include <string>
+
+#include "url/gurl.h"
+
+namespace google_apis {
+
+// The class is used to generate URLs for communicating with the WAPI server.
+// for production, and the local server for testing.
+class GDataWapiUrlGenerator {
+ public:
+ // The
+ GDataWapiUrlGenerator(const GURL& base_url, const GURL& base_download_url);
+ ~GDataWapiUrlGenerator();
+
+ // The base URL for communicating with the WAPI server for production.
+ static const char kBaseUrlForProduction[];
+
+ // The base URL for the file download server for production.
+ static const char kBaseDownloadUrlForProduction[];
+
+ // Adds additional parameters for API version, output content type and to
+ // show folders in the feed are added to document feed URLs.
+ static GURL AddStandardUrlParams(const GURL& url);
+
+ // Adds additional parameters for initiate uploading as well as standard
+ // url params (as AddStandardUrlParams above does).
+ static GURL AddInitiateUploadUrlParams(const GURL& url);
+
+ // Adds additional parameters for API version, output content type and to
+ // show folders in the feed are added to document feed URLs.
+ static GURL AddFeedUrlParams(const GURL& url,
+ int num_items_to_fetch);
+
+ // Generates a URL for getting the resource list feed.
+ //
+ // The parameters other than |search_string| are mutually exclusive.
+ // If |override_url| is non-empty, other parameters are ignored. Or if
+ // |override_url| is empty, others are not used. Besides, |search_string|
+ // cannot be set together with |start_changestamp|.
+ //
+ // override_url:
+ // By default, a hard-coded base URL of the WAPI server is used.
+ // The base URL can be overridden by |override_url|.
+ // This is used for handling continuation of feeds (2nd page and onward).
+ //
+ // start_changestamp
+ // If |start_changestamp| is 0, URL for a full feed is generated.
+ // If |start_changestamp| is non-zero, URL for a delta feed is generated.
+ //
+ // search_string
+ // If |search_string| is non-empty, q=... parameter is added, and
+ // max-results=... parameter is adjusted for a search.
+ //
+ // directory_resource_id:
+ // If |directory_resource_id| is non-empty, a URL for fetching documents in
+ // a particular directory is generated.
+ //
+ GURL GenerateResourceListUrl(
+ const GURL& override_url,
+ int64 start_changestamp,
+ const std::string& search_string,
+ const std::string& directory_resource_id) const;
+
+ // Generates a URL for searching resources by title (exact-match).
+ // |directory_resource_id| is optional parameter. When it is empty
+ // all the existing resources are target of the search. Otherwise,
+ // the search target is just under the directory with it.
+ GURL GenerateSearchByTitleUrl(
+ const std::string& title,
+ const std::string& directory_resource_id) const;
+
+ // Generates a URL for getting or editing the resource entry of
+ // the given resource ID.
+ GURL GenerateEditUrl(const std::string& resource_id) const;
+
+ // Generates a URL for getting or editing the resource entry of the
+ // given resource ID without query params.
+ // Note that, in order to access to the WAPI server, it is necessary to
+ // append some query parameters to the URL. GenerateEditUrl declared above
+ // should be used in such cases. This method is designed for constructing
+ // the data, such as xml element/attributes in request body containing
+ // edit urls.
+ GURL GenerateEditUrlWithoutParams(const std::string& resource_id) const;
+
+ // Generates a URL for getting or editing the resource entry of the given
+ // resource ID with additionally passed embed origin. This is used to fetch
+ // share urls for the sharing dialog to be embedded with the |embed_origin|
+ // origin.
+ GURL GenerateEditUrlWithEmbedOrigin(const std::string& resource_id,
+ const GURL& embed_origin) const;
+
+ // Generates a URL for editing the contents in the directory specified
+ // by the given resource ID.
+ GURL GenerateContentUrl(const std::string& resource_id) const;
+
+ // Generates a URL to remove an entry specified by |resource_id| from
+ // the directory specified by the given |parent_resource_id|.
+ GURL GenerateResourceUrlForRemoval(const std::string& parent_resource_id,
+ const std::string& resource_id) const;
+
+ // Generates a URL to initiate uploading a new file to a directory
+ // specified by |parent_resource_id|.
+ GURL GenerateInitiateUploadNewFileUrl(
+ const std::string& parent_resource_id) const;
+
+ // Generates a URL to initiate uploading file content to overwrite a
+ // file specified by |resource_id|.
+ GURL GenerateInitiateUploadExistingFileUrl(
+ const std::string& resource_id) const;
+
+ // Generates a URL for getting the root resource list feed.
+ // Used to make changes in the root directory (ex. create a directory in the
+ // root directory)
+ GURL GenerateResourceListRootUrl() const;
+
+ // Generates a URL for getting the account metadata feed.
+ // If |include_installed_apps| is set to true, the response will include the
+ // list of installed third party applications.
+ GURL GenerateAccountMetadataUrl(bool include_installed_apps) const;
+
+ // Generates a URL for downloading a file.
+ GURL GenerateDownloadFileUrl(const std::string& resource_id) const;
+
+ private:
+ const GURL base_url_;
+ const GURL base_download_url_;
+};
+
+} // namespace google_apis
+
+#endif // GOOGLE_APIS_DRIVE_GDATA_WAPI_URL_GENERATOR_H_
diff --git a/chromium/google_apis/drive/gdata_wapi_url_generator_unittest.cc b/chromium/google_apis/drive/gdata_wapi_url_generator_unittest.cc
new file mode 100644
index 00000000000..63db63d6e3e
--- /dev/null
+++ b/chromium/google_apis/drive/gdata_wapi_url_generator_unittest.cc
@@ -0,0 +1,222 @@
+// Copyright (c) 2012 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "google_apis/drive/gdata_wapi_url_generator.h"
+
+#include "testing/gtest/include/gtest/gtest.h"
+#include "url/gurl.h"
+
+namespace google_apis {
+
+class GDataWapiUrlGeneratorTest : public testing::Test {
+ public:
+ GDataWapiUrlGeneratorTest()
+ : url_generator_(
+ GURL(GDataWapiUrlGenerator::kBaseUrlForProduction),
+ GURL(GDataWapiUrlGenerator::kBaseDownloadUrlForProduction)) {
+ }
+
+ protected:
+ GDataWapiUrlGenerator url_generator_;
+};
+
+TEST_F(GDataWapiUrlGeneratorTest, AddStandardUrlParams) {
+ EXPECT_EQ("http://www.example.com/?v=3&alt=json&showroot=true",
+ GDataWapiUrlGenerator::AddStandardUrlParams(
+ GURL("http://www.example.com")).spec());
+}
+
+TEST_F(GDataWapiUrlGeneratorTest, AddInitiateUploadUrlParams) {
+ EXPECT_EQ("http://www.example.com/?convert=false&v=3&alt=json&showroot=true",
+ GDataWapiUrlGenerator::AddInitiateUploadUrlParams(
+ GURL("http://www.example.com")).spec());
+}
+
+TEST_F(GDataWapiUrlGeneratorTest, AddFeedUrlParams) {
+ EXPECT_EQ(
+ "http://www.example.com/?v=3&alt=json&showroot=true&"
+ "showfolders=true"
+ "&include-shared=true"
+ "&max-results=100",
+ GDataWapiUrlGenerator::AddFeedUrlParams(GURL("http://www.example.com"),
+ 100 // num_items_to_fetch
+ ).spec());
+}
+
+TEST_F(GDataWapiUrlGeneratorTest, GenerateResourceListUrl) {
+ // This is the very basic URL for the GetResourceList request.
+ EXPECT_EQ("https://docs.google.com/feeds/default/private/full"
+ "?v=3&alt=json&showroot=true&showfolders=true&include-shared=true"
+ "&max-results=500",
+ url_generator_.GenerateResourceListUrl(
+ GURL(), // override_url,
+ 0, // start_changestamp,
+ std::string(), // search_string,
+ std::string() // directory resource ID
+ ).spec());
+
+ // With an override URL provided, the base URL is changed, but the default
+ // parameters remain as-is.
+ EXPECT_EQ("http://localhost/"
+ "?v=3&alt=json&showroot=true&showfolders=true&include-shared=true"
+ "&max-results=500",
+ url_generator_.GenerateResourceListUrl(
+ GURL("http://localhost/"), // override_url,
+ 0, // start_changestamp,
+ std::string(), // search_string,
+ std::string() // directory resource ID
+ ).spec());
+
+ // With a non-zero start_changestamp provided, the base URL is changed from
+ // "full" to "changes", and "start-index" parameter is added.
+ EXPECT_EQ("https://docs.google.com/feeds/default/private/changes"
+ "?v=3&alt=json&showroot=true&showfolders=true&include-shared=true"
+ "&max-results=500&start-index=100",
+ url_generator_.GenerateResourceListUrl(
+ GURL(), // override_url,
+ 100, // start_changestamp,
+ std::string(), // search_string,
+ std::string() // directory resource ID
+ ).spec());
+
+ // With a non-empty search string provided, "max-results" value is changed,
+ // and "q" parameter is added.
+ EXPECT_EQ("https://docs.google.com/feeds/default/private/full"
+ "?v=3&alt=json&showroot=true&showfolders=true&include-shared=true"
+ "&max-results=50&q=foo",
+ url_generator_.GenerateResourceListUrl(
+ GURL(), // override_url,
+ 0, // start_changestamp,
+ "foo", // search_string,
+ std::string() // directory resource ID
+ ).spec());
+
+ // With a non-empty directory resource ID provided, the base URL is
+ // changed, but the default parameters remain.
+ EXPECT_EQ(
+ "https://docs.google.com/feeds/default/private/full/XXX/contents"
+ "?v=3&alt=json&showroot=true&showfolders=true&include-shared=true"
+ "&max-results=500",
+ url_generator_.GenerateResourceListUrl(GURL(), // override_url,
+ 0, // start_changestamp,
+ std::string(), // search_string,
+ "XXX" // directory resource ID
+ ).spec());
+
+ // With a non-empty override_url provided, the base URL is changed, but
+ // the default parameters remain. Note that start-index should not be
+ // overridden.
+ EXPECT_EQ("http://example.com/"
+ "?start-index=123&v=3&alt=json&showroot=true&showfolders=true"
+ "&include-shared=true&max-results=500",
+ url_generator_.GenerateResourceListUrl(
+ GURL("http://example.com/?start-index=123"), // override_url,
+ 100, // start_changestamp,
+ std::string(), // search_string,
+ "XXX" // directory resource ID
+ ).spec());
+}
+
+TEST_F(GDataWapiUrlGeneratorTest, GenerateSearchByTitleUrl) {
+ EXPECT_EQ(
+ "https://docs.google.com/feeds/default/private/full"
+ "?v=3&alt=json&showroot=true&showfolders=true&include-shared=true"
+ "&max-results=500&title=search-title&title-exact=true",
+ url_generator_.GenerateSearchByTitleUrl(
+ "search-title", std::string()).spec());
+
+ EXPECT_EQ(
+ "https://docs.google.com/feeds/default/private/full/XXX/contents"
+ "?v=3&alt=json&showroot=true&showfolders=true&include-shared=true"
+ "&max-results=500&title=search-title&title-exact=true",
+ url_generator_.GenerateSearchByTitleUrl(
+ "search-title", "XXX").spec());
+}
+
+TEST_F(GDataWapiUrlGeneratorTest, GenerateEditUrl) {
+ EXPECT_EQ(
+ "https://docs.google.com/feeds/default/private/full/XXX?v=3&alt=json"
+ "&showroot=true",
+ url_generator_.GenerateEditUrl("XXX").spec());
+}
+
+TEST_F(GDataWapiUrlGeneratorTest, GenerateEditUrlWithoutParams) {
+ EXPECT_EQ(
+ "https://docs.google.com/feeds/default/private/full/XXX",
+ url_generator_.GenerateEditUrlWithoutParams("XXX").spec());
+}
+
+TEST_F(GDataWapiUrlGeneratorTest, GenerateEditUrlWithEmbedOrigin) {
+ EXPECT_EQ(
+ "https://docs.google.com/feeds/default/private/full/XXX?v=3&alt=json"
+ "&showroot=true&embedOrigin=chrome-extension%3A%2F%2Ftest",
+ url_generator_.GenerateEditUrlWithEmbedOrigin(
+ "XXX",
+ GURL("chrome-extension://test")).spec());
+ EXPECT_EQ(
+ "https://docs.google.com/feeds/default/private/full/XXX?v=3&alt=json"
+ "&showroot=true",
+ url_generator_.GenerateEditUrlWithEmbedOrigin(
+ "XXX",
+ GURL()).spec());
+}
+
+TEST_F(GDataWapiUrlGeneratorTest, GenerateContentUrl) {
+ EXPECT_EQ(
+ "https://docs.google.com/feeds/default/private/full/"
+ "folder%3Aroot/contents?v=3&alt=json&showroot=true",
+ url_generator_.GenerateContentUrl("folder:root").spec());
+}
+
+TEST_F(GDataWapiUrlGeneratorTest, GenerateResourceUrlForRemoval) {
+ EXPECT_EQ(
+ "https://docs.google.com/feeds/default/private/full/"
+ "folder%3Aroot/contents/file%3AABCDE?v=3&alt=json&showroot=true",
+ url_generator_.GenerateResourceUrlForRemoval(
+ "folder:root", "file:ABCDE").spec());
+}
+
+TEST_F(GDataWapiUrlGeneratorTest, GenerateInitiateUploadNewFileUrl) {
+ EXPECT_EQ(
+ "https://docs.google.com/feeds/upload/create-session/default/private/"
+ "full/folder%3Aabcde/contents?convert=false&v=3&alt=json&showroot=true",
+ url_generator_.GenerateInitiateUploadNewFileUrl("folder:abcde").spec());
+}
+
+TEST_F(GDataWapiUrlGeneratorTest, GenerateInitiateUploadExistingFileUrl) {
+ EXPECT_EQ(
+ "https://docs.google.com/feeds/upload/create-session/default/private/"
+ "full/file%3Aresource_id?convert=false&v=3&alt=json&showroot=true",
+ url_generator_.GenerateInitiateUploadExistingFileUrl(
+ "file:resource_id").spec());
+}
+
+TEST_F(GDataWapiUrlGeneratorTest, GenerateResourceListRootUrl) {
+ EXPECT_EQ(
+ "https://docs.google.com/feeds/default/private/full?v=3&alt=json"
+ "&showroot=true",
+ url_generator_.GenerateResourceListRootUrl().spec());
+}
+
+TEST_F(GDataWapiUrlGeneratorTest, GenerateAccountMetadataUrl) {
+ // Include installed apps.
+ EXPECT_EQ(
+ "https://docs.google.com/feeds/metadata/default"
+ "?v=3&alt=json&showroot=true&include-installed-apps=true",
+ url_generator_.GenerateAccountMetadataUrl(true).spec());
+
+ // Exclude installed apps.
+ EXPECT_EQ(
+ "https://docs.google.com/feeds/metadata/default?v=3&alt=json"
+ "&showroot=true",
+ url_generator_.GenerateAccountMetadataUrl(false).spec());
+}
+
+TEST_F(GDataWapiUrlGeneratorTest, GenerateDownloadFileUrl) {
+ EXPECT_EQ(
+ "https://www.googledrive.com/host/resourceId",
+ url_generator_.GenerateDownloadFileUrl("file:resourceId").spec());
+}
+
+} // namespace google_apis
diff --git a/chromium/google_apis/drive/request_sender.cc b/chromium/google_apis/drive/request_sender.cc
new file mode 100644
index 00000000000..dbf5d85860f
--- /dev/null
+++ b/chromium/google_apis/drive/request_sender.cc
@@ -0,0 +1,105 @@
+// Copyright (c) 2012 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "google_apis/drive/request_sender.h"
+
+#include "base/bind.h"
+#include "base/sequenced_task_runner.h"
+#include "base/stl_util.h"
+#include "google_apis/drive/auth_service.h"
+#include "google_apis/drive/base_requests.h"
+#include "net/url_request/url_request_context_getter.h"
+
+namespace google_apis {
+
+RequestSender::RequestSender(
+ AuthServiceInterface* auth_service,
+ net::URLRequestContextGetter* url_request_context_getter,
+ base::SequencedTaskRunner* blocking_task_runner,
+ const std::string& custom_user_agent)
+ : auth_service_(auth_service),
+ url_request_context_getter_(url_request_context_getter),
+ blocking_task_runner_(blocking_task_runner),
+ custom_user_agent_(custom_user_agent),
+ weak_ptr_factory_(this) {
+}
+
+RequestSender::~RequestSender() {
+ DCHECK(thread_checker_.CalledOnValidThread());
+ STLDeleteContainerPointers(in_flight_requests_.begin(),
+ in_flight_requests_.end());
+}
+
+base::Closure RequestSender::StartRequestWithRetry(
+ AuthenticatedRequestInterface* request) {
+ DCHECK(thread_checker_.CalledOnValidThread());
+
+ in_flight_requests_.insert(request);
+
+ // TODO(kinaba): Stop relying on weak pointers. Move lifetime management
+ // of the requests to request sender.
+ base::Closure cancel_closure =
+ base::Bind(&RequestSender::CancelRequest,
+ weak_ptr_factory_.GetWeakPtr(),
+ request->GetWeakPtr());
+
+ if (!auth_service_->HasAccessToken()) {
+ // Fetch OAuth2 access token from the refresh token first.
+ auth_service_->StartAuthentication(
+ base::Bind(&RequestSender::OnAccessTokenFetched,
+ weak_ptr_factory_.GetWeakPtr(),
+ request->GetWeakPtr()));
+ } else {
+ request->Start(auth_service_->access_token(),
+ custom_user_agent_,
+ base::Bind(&RequestSender::RetryRequest,
+ weak_ptr_factory_.GetWeakPtr()));
+ }
+
+ return cancel_closure;
+}
+
+void RequestSender::OnAccessTokenFetched(
+ const base::WeakPtr<AuthenticatedRequestInterface>& request,
+ GDataErrorCode code,
+ const std::string& /* access_token */) {
+ DCHECK(thread_checker_.CalledOnValidThread());
+
+ // Do nothing if the request is canceled during authentication.
+ if (!request.get())
+ return;
+
+ if (code == HTTP_SUCCESS) {
+ DCHECK(auth_service_->HasAccessToken());
+ StartRequestWithRetry(request.get());
+ } else {
+ request->OnAuthFailed(code);
+ }
+}
+
+void RequestSender::RetryRequest(AuthenticatedRequestInterface* request) {
+ DCHECK(thread_checker_.CalledOnValidThread());
+
+ auth_service_->ClearAccessToken();
+ // User authentication might have expired - rerun the request to force
+ // auth token refresh.
+ StartRequestWithRetry(request);
+}
+
+void RequestSender::CancelRequest(
+ const base::WeakPtr<AuthenticatedRequestInterface>& request) {
+ DCHECK(thread_checker_.CalledOnValidThread());
+
+ // Do nothing if the request is already finished.
+ if (!request.get())
+ return;
+ request->Cancel();
+}
+
+void RequestSender::RequestFinished(AuthenticatedRequestInterface* request) {
+ in_flight_requests_.erase(request);
+ delete request;
+}
+
+} // namespace google_apis
diff --git a/chromium/google_apis/drive/request_sender.h b/chromium/google_apis/drive/request_sender.h
new file mode 100644
index 00000000000..dfc671c42bf
--- /dev/null
+++ b/chromium/google_apis/drive/request_sender.h
@@ -0,0 +1,111 @@
+// Copyright (c) 2012 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef GOOGLE_APIS_DRIVE_REQUEST_SENDER_H_
+#define GOOGLE_APIS_DRIVE_REQUEST_SENDER_H_
+
+#include <set>
+#include <string>
+
+#include "base/basictypes.h"
+#include "base/callback_forward.h"
+#include "base/memory/ref_counted.h"
+#include "base/memory/scoped_ptr.h"
+#include "base/memory/weak_ptr.h"
+#include "base/threading/thread_checker.h"
+#include "google_apis/drive/gdata_errorcode.h"
+
+namespace base {
+class SequencedTaskRunner;
+}
+
+namespace net {
+class URLRequestContextGetter;
+}
+
+namespace google_apis {
+
+class AuthenticatedRequestInterface;
+class AuthServiceInterface;
+
+// Helper class that sends requests implementing
+// AuthenticatedRequestInterface and handles retries and authentication.
+class RequestSender {
+ public:
+ // |auth_service| is used for fetching OAuth tokens. It'll be owned by
+ // this RequestSender.
+ //
+ // |url_request_context_getter| is the context used to perform network
+ // requests from this RequestSender.
+ //
+ // |blocking_task_runner| is used for running blocking operation, e.g.,
+ // parsing JSON response from the server.
+ //
+ // |custom_user_agent| will be used for the User-Agent header in HTTP
+ // requests issued through the request sender if the value is not empty.
+ RequestSender(AuthServiceInterface* auth_service,
+ net::URLRequestContextGetter* url_request_context_getter,
+ base::SequencedTaskRunner* blocking_task_runner,
+ const std::string& custom_user_agent);
+ ~RequestSender();
+
+ AuthServiceInterface* auth_service() { return auth_service_.get(); }
+
+ net::URLRequestContextGetter* url_request_context_getter() const {
+ return url_request_context_getter_;
+ }
+
+ base::SequencedTaskRunner* blocking_task_runner() const {
+ return blocking_task_runner_.get();
+ }
+
+ // Starts a request implementing the AuthenticatedRequestInterface
+ // interface, and makes the request retry upon authentication failures by
+ // calling back to RetryRequest. The |request| object is owned by this
+ // RequestSender. It will be deleted in RequestSender's destructor or
+ // in RequestFinished().
+ //
+ // Returns a closure to cancel the request. The closure cancels the request
+ // if it is in-flight, and does nothing if it is already terminated.
+ base::Closure StartRequestWithRetry(AuthenticatedRequestInterface* request);
+
+ // Notifies to this RequestSender that |request| has finished.
+ // TODO(kinaba): refactor the life time management and make this at private.
+ void RequestFinished(AuthenticatedRequestInterface* request);
+
+ private:
+ // Called when the access token is fetched.
+ void OnAccessTokenFetched(
+ const base::WeakPtr<AuthenticatedRequestInterface>& request,
+ GDataErrorCode error,
+ const std::string& access_token);
+
+ // Clears any authentication token and retries the request, which forces
+ // an authentication token refresh.
+ void RetryRequest(AuthenticatedRequestInterface* request);
+
+ // Cancels the request. Used for implementing the returned closure of
+ // StartRequestWithRetry.
+ void CancelRequest(
+ const base::WeakPtr<AuthenticatedRequestInterface>& request);
+
+ scoped_ptr<AuthServiceInterface> auth_service_;
+ scoped_refptr<net::URLRequestContextGetter> url_request_context_getter_;
+ scoped_refptr<base::SequencedTaskRunner> blocking_task_runner_;
+
+ std::set<AuthenticatedRequestInterface*> in_flight_requests_;
+ const std::string custom_user_agent_;
+
+ base::ThreadChecker thread_checker_;
+
+ // Note: This should remain the last member so it'll be destroyed and
+ // invalidate its weak pointers before any other members are destroyed.
+ base::WeakPtrFactory<RequestSender> weak_ptr_factory_;
+
+ DISALLOW_COPY_AND_ASSIGN(RequestSender);
+};
+
+} // namespace google_apis
+
+#endif // GOOGLE_APIS_DRIVE_REQUEST_SENDER_H_
diff --git a/chromium/google_apis/drive/request_sender_unittest.cc b/chromium/google_apis/drive/request_sender_unittest.cc
new file mode 100644
index 00000000000..20f5402ef11
--- /dev/null
+++ b/chromium/google_apis/drive/request_sender_unittest.cc
@@ -0,0 +1,250 @@
+// Copyright 2013 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "google_apis/drive/request_sender.h"
+
+#include "base/strings/string_number_conversions.h"
+#include "google_apis/drive/base_requests.h"
+#include "google_apis/drive/dummy_auth_service.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+namespace google_apis {
+
+namespace {
+
+const char kTestRefreshToken[] = "valid-refresh-token";
+const char kTestAccessToken[] = "valid-access-token";
+
+// Enum for indicating the reason why a request is finished.
+enum FinishReason {
+ NONE,
+ SUCCESS,
+ CANCEL,
+ AUTH_FAILURE,
+};
+
+// AuthService for testing purpose. It accepts kTestRefreshToken and returns
+// kTestAccessToken + {"1", "2", "3", ...}.
+class TestAuthService : public DummyAuthService {
+ public:
+ TestAuthService() : auth_try_count_(0) {}
+
+ virtual void StartAuthentication(
+ const AuthStatusCallback& callback) OVERRIDE {
+ // RequestSender should clear the rejected access token before starting
+ // to request another one.
+ EXPECT_FALSE(HasAccessToken());
+
+ ++auth_try_count_;
+
+ if (refresh_token() == kTestRefreshToken) {
+ const std::string token =
+ kTestAccessToken + base::IntToString(auth_try_count_);
+ set_access_token(token);
+ callback.Run(HTTP_SUCCESS, token);
+ } else {
+ set_access_token("");
+ callback.Run(HTTP_UNAUTHORIZED, "");
+ }
+ }
+
+ private:
+ int auth_try_count_;
+};
+
+// The main test fixture class.
+class RequestSenderTest : public testing::Test {
+ protected:
+ RequestSenderTest()
+ : auth_service_(new TestAuthService),
+ request_sender_(auth_service_, NULL, NULL, "dummy-user-agent") {
+ auth_service_->set_refresh_token(kTestRefreshToken);
+ auth_service_->set_access_token(kTestAccessToken);
+ }
+
+ TestAuthService* auth_service_; // Owned by |request_sender_|.
+ RequestSender request_sender_;
+};
+
+// Minimal implementation for AuthenticatedRequestInterface that can interact
+// with RequestSender correctly.
+class TestRequest : public AuthenticatedRequestInterface {
+ public:
+ TestRequest(RequestSender* sender,
+ bool* start_called,
+ FinishReason* finish_reason)
+ : sender_(sender),
+ start_called_(start_called),
+ finish_reason_(finish_reason),
+ weak_ptr_factory_(this) {
+ }
+
+ // Test the situation that the request has finished.
+ void FinishRequestWithSuccess() {
+ *finish_reason_ = SUCCESS;
+ sender_->RequestFinished(this);
+ }
+
+ const std::string& passed_access_token() const {
+ return passed_access_token_;
+ }
+
+ const ReAuthenticateCallback& passed_reauth_callback() const {
+ return passed_reauth_callback_;
+ }
+
+ virtual void Start(const std::string& access_token,
+ const std::string& custom_user_agent,
+ const ReAuthenticateCallback& callback) OVERRIDE {
+ *start_called_ = true;
+ passed_access_token_ = access_token;
+ passed_reauth_callback_ = callback;
+
+ // This request class itself does not return any response at this point.
+ // Each test case should respond properly by using the above methods.
+ }
+
+ virtual void Cancel() OVERRIDE {
+ EXPECT_TRUE(*start_called_);
+ *finish_reason_ = CANCEL;
+ sender_->RequestFinished(this);
+ }
+
+ virtual void OnAuthFailed(GDataErrorCode code) OVERRIDE {
+ *finish_reason_ = AUTH_FAILURE;
+ sender_->RequestFinished(this);
+ }
+
+ virtual base::WeakPtr<AuthenticatedRequestInterface> GetWeakPtr() OVERRIDE {
+ return weak_ptr_factory_.GetWeakPtr();
+ }
+
+ private:
+ RequestSender* sender_;
+ bool* start_called_;
+ FinishReason* finish_reason_;
+ std::string passed_access_token_;
+ ReAuthenticateCallback passed_reauth_callback_;
+ base::WeakPtrFactory<TestRequest> weak_ptr_factory_;
+};
+
+} // namespace
+
+TEST_F(RequestSenderTest, StartAndFinishRequest) {
+ bool start_called = false;
+ FinishReason finish_reason = NONE;
+ TestRequest* request = new TestRequest(&request_sender_,
+ &start_called,
+ &finish_reason);
+ base::WeakPtr<AuthenticatedRequestInterface> weak_ptr = request->GetWeakPtr();
+
+ base::Closure cancel_closure = request_sender_.StartRequestWithRetry(request);
+ EXPECT_TRUE(!cancel_closure.is_null());
+
+ // Start is called with the specified access token. Let it succeed.
+ EXPECT_TRUE(start_called);
+ EXPECT_EQ(kTestAccessToken, request->passed_access_token());
+ request->FinishRequestWithSuccess();
+ EXPECT_FALSE(weak_ptr); // The request object is deleted.
+
+ // It is safe to run the cancel closure even after the request is finished.
+ // It is just no-op. The TestRequest::Cancel method is not called.
+ cancel_closure.Run();
+ EXPECT_EQ(SUCCESS, finish_reason);
+}
+
+TEST_F(RequestSenderTest, StartAndCancelRequest) {
+ bool start_called = false;
+ FinishReason finish_reason = NONE;
+ TestRequest* request = new TestRequest(&request_sender_,
+ &start_called,
+ &finish_reason);
+ base::WeakPtr<AuthenticatedRequestInterface> weak_ptr = request->GetWeakPtr();
+
+ base::Closure cancel_closure = request_sender_.StartRequestWithRetry(request);
+ EXPECT_TRUE(!cancel_closure.is_null());
+ EXPECT_TRUE(start_called);
+
+ cancel_closure.Run();
+ EXPECT_EQ(CANCEL, finish_reason);
+ EXPECT_FALSE(weak_ptr); // The request object is deleted.
+}
+
+TEST_F(RequestSenderTest, NoRefreshToken) {
+ auth_service_->ClearRefreshToken();
+ auth_service_->ClearAccessToken();
+
+ bool start_called = false;
+ FinishReason finish_reason = NONE;
+ TestRequest* request = new TestRequest(&request_sender_,
+ &start_called,
+ &finish_reason);
+ base::WeakPtr<AuthenticatedRequestInterface> weak_ptr = request->GetWeakPtr();
+
+ base::Closure cancel_closure = request_sender_.StartRequestWithRetry(request);
+ EXPECT_TRUE(!cancel_closure.is_null());
+
+ // The request is not started at all because no access token is obtained.
+ EXPECT_FALSE(start_called);
+ EXPECT_EQ(AUTH_FAILURE, finish_reason);
+ EXPECT_FALSE(weak_ptr); // The request object is deleted.
+}
+
+TEST_F(RequestSenderTest, ValidRefreshTokenAndNoAccessToken) {
+ auth_service_->ClearAccessToken();
+
+ bool start_called = false;
+ FinishReason finish_reason = NONE;
+ TestRequest* request = new TestRequest(&request_sender_,
+ &start_called,
+ &finish_reason);
+ base::WeakPtr<AuthenticatedRequestInterface> weak_ptr = request->GetWeakPtr();
+
+ base::Closure cancel_closure = request_sender_.StartRequestWithRetry(request);
+ EXPECT_TRUE(!cancel_closure.is_null());
+
+ // Access token should indicate that this is the first retry.
+ EXPECT_TRUE(start_called);
+ EXPECT_EQ(kTestAccessToken + std::string("1"),
+ request->passed_access_token());
+ request->FinishRequestWithSuccess();
+ EXPECT_EQ(SUCCESS, finish_reason);
+ EXPECT_FALSE(weak_ptr); // The request object is deleted.
+}
+
+TEST_F(RequestSenderTest, AccessTokenRejectedSeveralTimes) {
+ bool start_called = false;
+ FinishReason finish_reason = NONE;
+ TestRequest* request = new TestRequest(&request_sender_,
+ &start_called,
+ &finish_reason);
+ base::WeakPtr<AuthenticatedRequestInterface> weak_ptr = request->GetWeakPtr();
+
+ base::Closure cancel_closure = request_sender_.StartRequestWithRetry(request);
+ EXPECT_TRUE(!cancel_closure.is_null());
+
+ EXPECT_TRUE(start_called);
+ EXPECT_EQ(kTestAccessToken, request->passed_access_token());
+ // Emulate the case that the access token was rejected by the remote service.
+ request->passed_reauth_callback().Run(request);
+ // New access token is fetched. Let it fail once again.
+ EXPECT_EQ(kTestAccessToken + std::string("1"),
+ request->passed_access_token());
+ request->passed_reauth_callback().Run(request);
+ // Once more.
+ EXPECT_EQ(kTestAccessToken + std::string("2"),
+ request->passed_access_token());
+ request->passed_reauth_callback().Run(request);
+
+ // Currently, limit for the retry is controlled in each request object, not
+ // by the RequestSender. So with this TestRequest, RequestSender retries
+ // infinitely. Let it succeed/
+ EXPECT_EQ(kTestAccessToken + std::string("3"),
+ request->passed_access_token());
+ request->FinishRequestWithSuccess();
+ EXPECT_EQ(SUCCESS, finish_reason);
+ EXPECT_FALSE(weak_ptr);
+}
+
+} // namespace google_apis
diff --git a/chromium/google_apis/drive/request_util.cc b/chromium/google_apis/drive/request_util.cc
new file mode 100644
index 00000000000..9737a6f90d5
--- /dev/null
+++ b/chromium/google_apis/drive/request_util.cc
@@ -0,0 +1,26 @@
+// Copyright (c) 2013 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "google_apis/drive/request_util.h"
+
+#include <string>
+
+namespace google_apis {
+namespace util {
+
+namespace {
+
+// etag matching header.
+const char kIfMatchHeaderPrefix[] = "If-Match: ";
+
+} // namespace
+
+const char kIfMatchAllHeader[] = "If-Match: *";
+
+std::string GenerateIfMatchHeader(const std::string& etag) {
+ return etag.empty() ? kIfMatchAllHeader : (kIfMatchHeaderPrefix + etag);
+}
+
+} // namespace util
+} // namespace google_apis
diff --git a/chromium/google_apis/drive/request_util.h b/chromium/google_apis/drive/request_util.h
new file mode 100644
index 00000000000..f0672edaf7a
--- /dev/null
+++ b/chromium/google_apis/drive/request_util.h
@@ -0,0 +1,23 @@
+// Copyright (c) 2013 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef GOOGLE_APIS_DRIVE_REQUEST_UTIL_H_
+#define GOOGLE_APIS_DRIVE_REQUEST_UTIL_H_
+
+#include <string>
+
+namespace google_apis {
+namespace util {
+
+// If-Match header which matches to all etags.
+extern const char kIfMatchAllHeader[];
+
+// Returns If-Match header string for |etag|.
+// If |etag| is empty, the returned header should match any etag.
+std::string GenerateIfMatchHeader(const std::string& etag);
+
+} // namespace util
+} // namespace google_apis
+
+#endif // GOOGLE_APIS_DRIVE_REQUEST_UTIL_H_
diff --git a/chromium/google_apis/drive/request_util_unittest.cc b/chromium/google_apis/drive/request_util_unittest.cc
new file mode 100644
index 00000000000..f1e703c8e93
--- /dev/null
+++ b/chromium/google_apis/drive/request_util_unittest.cc
@@ -0,0 +1,22 @@
+// Copyright (c) 2013 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "google_apis/drive/request_util.h"
+
+#include "testing/gtest/include/gtest/gtest.h"
+
+namespace google_apis {
+namespace util {
+
+TEST(GenerateIfMatchHeaderTest, GenerateIfMatchHeader) {
+ // The header matched to all etag should be returned for empty etag.
+ EXPECT_EQ("If-Match: *", GenerateIfMatchHeader(std::string()));
+
+ // Otherwise, the returned header should be matched to the given etag.
+ EXPECT_EQ("If-Match: abcde", GenerateIfMatchHeader("abcde"));
+ EXPECT_EQ("If-Match: fake_etag", GenerateIfMatchHeader("fake_etag"));
+}
+
+} // namespace util
+} // namespace google_apis
diff --git a/chromium/google_apis/drive/task_util.cc b/chromium/google_apis/drive/task_util.cc
new file mode 100644
index 00000000000..3f149e434ef
--- /dev/null
+++ b/chromium/google_apis/drive/task_util.cc
@@ -0,0 +1,21 @@
+// Copyright (c) 2012 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "google_apis/drive/task_util.h"
+
+#include "base/location.h"
+
+namespace google_apis {
+
+void RunTaskOnThread(scoped_refptr<base::SingleThreadTaskRunner> task_runner,
+ const base::Closure& task) {
+ if (task_runner->BelongsToCurrentThread()) {
+ task.Run();
+ } else {
+ const bool posted = task_runner->PostTask(FROM_HERE, task);
+ DCHECK(posted);
+ }
+}
+
+} // namespace google_apis
diff --git a/chromium/google_apis/drive/task_util.h b/chromium/google_apis/drive/task_util.h
new file mode 100644
index 00000000000..443f719694e
--- /dev/null
+++ b/chromium/google_apis/drive/task_util.h
@@ -0,0 +1,136 @@
+// Copyright (c) 2012 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef GOOGLE_APIS_DRIVE_TASK_UTIL_H_
+#define GOOGLE_APIS_DRIVE_TASK_UTIL_H_
+
+#include "base/bind.h"
+#include "base/message_loop/message_loop_proxy.h"
+
+namespace google_apis {
+
+// Runs task on the thread to which |task_runner| belongs.
+void RunTaskOnThread(scoped_refptr<base::SingleThreadTaskRunner> task_runner,
+ const base::Closure& task);
+
+namespace internal {
+
+// Implementation of the composed callback, whose signature is |Sig|.
+template<typename Sig> struct ComposedCallback;
+
+// ComposedCallback with no argument.
+template<>
+struct ComposedCallback<void()> {
+ static void Run(
+ const base::Callback<void(const base::Closure&)>& runner,
+ const base::Closure& callback) {
+ runner.Run(callback);
+ }
+};
+
+// ComposedCallback with one argument.
+template<typename T1>
+struct ComposedCallback<void(T1)> {
+ static void Run(
+ const base::Callback<void(const base::Closure&)>& runner,
+ const base::Callback<void(T1)>& callback,
+ T1 arg1) {
+ runner.Run(base::Bind(callback, arg1));
+ }
+};
+
+// ComposedCallback with two arguments.
+template<typename T1, typename T2>
+struct ComposedCallback<void(T1, T2)> {
+ static void Run(
+ const base::Callback<void(const base::Closure&)>& runner,
+ const base::Callback<void(T1, T2)>& callback,
+ T1 arg1, T2 arg2) {
+ runner.Run(base::Bind(callback, arg1, arg2));
+ }
+};
+
+// ComposedCallback with two arguments, and the last one is scoped_ptr.
+template<typename T1, typename T2, typename D2>
+struct ComposedCallback<void(T1, scoped_ptr<T2, D2>)> {
+ static void Run(
+ const base::Callback<void(const base::Closure&)>& runner,
+ const base::Callback<void(T1, scoped_ptr<T2, D2>)>& callback,
+ T1 arg1, scoped_ptr<T2, D2> arg2) {
+ runner.Run(base::Bind(callback, arg1, base::Passed(&arg2)));
+ }
+};
+
+// ComposedCallback with three arguments.
+template<typename T1, typename T2, typename T3>
+struct ComposedCallback<void(T1, T2, T3)> {
+ static void Run(
+ const base::Callback<void(const base::Closure&)>& runner,
+ const base::Callback<void(T1, T2, T3)>& callback,
+ T1 arg1, T2 arg2, T3 arg3) {
+ runner.Run(base::Bind(callback, arg1, arg2, arg3));
+ }
+};
+
+// ComposedCallback with three arguments, and the last one is scoped_ptr.
+template<typename T1, typename T2, typename T3, typename D3>
+struct ComposedCallback<void(T1, T2, scoped_ptr<T3, D3>)> {
+ static void Run(
+ const base::Callback<void(const base::Closure&)>& runner,
+ const base::Callback<void(T1, T2, scoped_ptr<T3, D3>)>& callback,
+ T1 arg1, T2 arg2, scoped_ptr<T3, D3> arg3) {
+ runner.Run(base::Bind(callback, arg1, arg2, base::Passed(&arg3)));
+ }
+};
+
+// ComposedCallback with four arguments.
+template<typename T1, typename T2, typename T3, typename T4>
+struct ComposedCallback<void(T1, T2, T3, T4)> {
+ static void Run(
+ const base::Callback<void(const base::Closure&)>& runner,
+ const base::Callback<void(T1, T2, T3, T4)>& callback,
+ T1 arg1, T2 arg2, T3 arg3, T4 arg4) {
+ runner.Run(base::Bind(callback, arg1, arg2, arg3, arg4));
+ }
+};
+
+// ComposedCallback with four arguments, and the second one is scoped_ptr.
+template<typename T1, typename T2, typename D2, typename T3, typename T4>
+struct ComposedCallback<void(T1, scoped_ptr<T2, D2>, T3, T4)> {
+ static void Run(
+ const base::Callback<void(const base::Closure&)>& runner,
+ const base::Callback<void(T1, scoped_ptr<T2, D2>, T3, T4)>& callback,
+ T1 arg1, scoped_ptr<T2, D2> arg2, T3 arg3, T4 arg4) {
+ runner.Run(base::Bind(callback, arg1, base::Passed(&arg2), arg3, arg4));
+ }
+};
+
+} // namespace internal
+
+// Returns callback that takes arguments (arg1, arg2, ...), create a closure
+// by binding them to |callback|, and runs |runner| with the closure.
+// I.e. the returned callback works as follows:
+// runner.Run(Bind(callback, arg1, arg2, ...))
+template<typename CallbackType>
+CallbackType CreateComposedCallback(
+ const base::Callback<void(const base::Closure&)>& runner,
+ const CallbackType& callback) {
+ DCHECK(!runner.is_null());
+ DCHECK(!callback.is_null());
+ return base::Bind(
+ &internal::ComposedCallback<typename CallbackType::RunType>::Run,
+ runner, callback);
+}
+
+// Returns callback which runs the given |callback| on the current thread.
+template<typename CallbackType>
+CallbackType CreateRelayCallback(const CallbackType& callback) {
+ return CreateComposedCallback(
+ base::Bind(&RunTaskOnThread, base::MessageLoopProxy::current()),
+ callback);
+}
+
+} // namespace google_apis
+
+#endif // GOOGLE_APIS_DRIVE_TASK_UTIL_H_
diff --git a/chromium/google_apis/drive/test_util.cc b/chromium/google_apis/drive/test_util.cc
new file mode 100644
index 00000000000..20d8ad65a6f
--- /dev/null
+++ b/chromium/google_apis/drive/test_util.cc
@@ -0,0 +1,183 @@
+// Copyright (c) 2012 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "google_apis/drive/test_util.h"
+
+#include "base/file_util.h"
+#include "base/json/json_file_value_serializer.h"
+#include "base/json/json_reader.h"
+#include "base/message_loop/message_loop.h"
+#include "base/path_service.h"
+#include "base/rand_util.h"
+#include "base/run_loop.h"
+#include "base/strings/string_number_conversions.h"
+#include "base/strings/string_split.h"
+#include "base/strings/string_util.h"
+#include "base/strings/stringprintf.h"
+#include "google_apis/drive/drive_api_parser.h"
+#include "google_apis/drive/gdata_wapi_parser.h"
+#include "google_apis/drive/gdata_wapi_requests.h"
+#include "net/test/embedded_test_server/http_request.h"
+#include "net/test/embedded_test_server/http_response.h"
+#include "url/gurl.h"
+
+namespace google_apis {
+namespace test_util {
+
+bool RemovePrefix(const std::string& input,
+ const std::string& prefix,
+ std::string* output) {
+ if (!StartsWithASCII(input, prefix, true /* case sensitive */))
+ return false;
+
+ *output = input.substr(prefix.size());
+ return true;
+}
+
+base::FilePath GetTestFilePath(const std::string& relative_path) {
+ base::FilePath path;
+ if (!PathService::Get(base::DIR_SOURCE_ROOT, &path))
+ return base::FilePath();
+ path = path.AppendASCII("chrome")
+ .AppendASCII("test")
+ .AppendASCII("data")
+ .Append(base::FilePath::FromUTF8Unsafe(relative_path));
+ return path;
+}
+
+GURL GetBaseUrlForTesting(int port) {
+ return GURL(base::StringPrintf("http://127.0.0.1:%d/", port));
+}
+
+void RunAndQuit(base::RunLoop* run_loop, const base::Closure& closure) {
+ closure.Run();
+ run_loop->Quit();
+}
+
+bool WriteStringToFile(const base::FilePath& file_path,
+ const std::string& content) {
+ int result = file_util::WriteFile(file_path, content.data(), content.size());
+ return content.size() == static_cast<size_t>(result);
+}
+
+bool CreateFileOfSpecifiedSize(const base::FilePath& temp_dir,
+ size_t size,
+ base::FilePath* path,
+ std::string* data) {
+ if (!base::CreateTemporaryFileInDir(temp_dir, path))
+ return false;
+
+ if (size == 0) {
+ // Note: RandBytesAsString doesn't support generating an empty string.
+ data->clear();
+ return true;
+ }
+
+ *data = base::RandBytesAsString(size);
+ return WriteStringToFile(*path, *data);
+}
+
+scoped_ptr<base::Value> LoadJSONFile(const std::string& relative_path) {
+ base::FilePath path = GetTestFilePath(relative_path);
+
+ std::string error;
+ JSONFileValueSerializer serializer(path);
+ scoped_ptr<base::Value> value(serializer.Deserialize(NULL, &error));
+ LOG_IF(WARNING, !value.get()) << "Failed to parse " << path.value()
+ << ": " << error;
+ return value.Pass();
+}
+
+// Returns a HttpResponse created from the given file path.
+scoped_ptr<net::test_server::BasicHttpResponse> CreateHttpResponseFromFile(
+ const base::FilePath& file_path) {
+ std::string content;
+ if (!base::ReadFileToString(file_path, &content))
+ return scoped_ptr<net::test_server::BasicHttpResponse>();
+
+ std::string content_type = "text/plain";
+ if (EndsWith(file_path.AsUTF8Unsafe(), ".json", true /* case sensitive */))
+ content_type = "application/json";
+
+ scoped_ptr<net::test_server::BasicHttpResponse> http_response(
+ new net::test_server::BasicHttpResponse);
+ http_response->set_code(net::HTTP_OK);
+ http_response->set_content(content);
+ http_response->set_content_type(content_type);
+ return http_response.Pass();
+}
+
+scoped_ptr<net::test_server::HttpResponse> HandleDownloadFileRequest(
+ const GURL& base_url,
+ net::test_server::HttpRequest* out_request,
+ const net::test_server::HttpRequest& request) {
+ *out_request = request;
+
+ GURL absolute_url = base_url.Resolve(request.relative_url);
+ std::string remaining_path;
+ if (!RemovePrefix(absolute_url.path(), "/files/", &remaining_path))
+ return scoped_ptr<net::test_server::HttpResponse>();
+ return CreateHttpResponseFromFile(
+ GetTestFilePath(remaining_path)).PassAs<net::test_server::HttpResponse>();
+}
+
+bool ParseContentRangeHeader(const std::string& value,
+ int64* start_position,
+ int64* end_position,
+ int64* length) {
+ DCHECK(start_position);
+ DCHECK(end_position);
+ DCHECK(length);
+
+ std::string remaining;
+ if (!RemovePrefix(value, "bytes ", &remaining))
+ return false;
+
+ std::vector<std::string> parts;
+ base::SplitString(remaining, '/', &parts);
+ if (parts.size() != 2U)
+ return false;
+
+ const std::string range = parts[0];
+ if (!base::StringToInt64(parts[1], length))
+ return false;
+
+ parts.clear();
+ base::SplitString(range, '-', &parts);
+ if (parts.size() != 2U)
+ return false;
+
+ return (base::StringToInt64(parts[0], start_position) &&
+ base::StringToInt64(parts[1], end_position));
+}
+
+void AppendProgressCallbackResult(std::vector<ProgressInfo>* progress_values,
+ int64 progress,
+ int64 total) {
+ progress_values->push_back(ProgressInfo(progress, total));
+}
+
+TestGetContentCallback::TestGetContentCallback()
+ : callback_(base::Bind(&TestGetContentCallback::OnGetContent,
+ base::Unretained(this))) {
+}
+
+TestGetContentCallback::~TestGetContentCallback() {
+}
+
+std::string TestGetContentCallback::GetConcatenatedData() const {
+ std::string result;
+ for (size_t i = 0; i < data_.size(); ++i) {
+ result += *data_[i];
+ }
+ return result;
+}
+
+void TestGetContentCallback::OnGetContent(google_apis::GDataErrorCode error,
+ scoped_ptr<std::string> data) {
+ data_.push_back(data.release());
+}
+
+} // namespace test_util
+} // namespace google_apis
diff --git a/chromium/google_apis/drive/test_util.h b/chromium/google_apis/drive/test_util.h
new file mode 100644
index 00000000000..2cd85b62dd3
--- /dev/null
+++ b/chromium/google_apis/drive/test_util.h
@@ -0,0 +1,299 @@
+// Copyright (c) 2012 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef GOOGLE_APIS_DRIVE_TEST_UTIL_H_
+#define GOOGLE_APIS_DRIVE_TEST_UTIL_H_
+
+#include <string>
+#include <utility>
+#include <vector>
+
+#include "base/bind.h"
+#include "base/callback.h"
+#include "base/memory/scoped_ptr.h"
+#include "base/memory/scoped_vector.h"
+#include "base/template_util.h"
+#include "google_apis/drive/base_requests.h"
+#include "google_apis/drive/gdata_errorcode.h"
+#include "google_apis/drive/task_util.h"
+
+class GURL;
+
+namespace base {
+class FilePath;
+class RunLoop;
+class Value;
+}
+
+namespace net {
+namespace test_server {
+class BasicHttpResponse;
+class HttpResponse;
+struct HttpRequest;
+}
+}
+
+namespace google_apis {
+namespace test_util {
+
+// Runs the closure, and then quits the |run_loop|.
+void RunAndQuit(base::RunLoop* run_loop, const base::Closure& closure);
+
+// Returns callback which runs the given |callback| and then quits |run_loop|.
+template<typename CallbackType>
+CallbackType CreateQuitCallback(base::RunLoop* run_loop,
+ const CallbackType& callback) {
+ return CreateComposedCallback(base::Bind(&RunAndQuit, run_loop), callback);
+}
+
+// Removes |prefix| from |input| and stores the result in |output|. Returns
+// true if the prefix is removed.
+bool RemovePrefix(const std::string& input,
+ const std::string& prefix,
+ std::string* output);
+
+// Returns the absolute path for a test file stored under
+// chrome/test/data.
+base::FilePath GetTestFilePath(const std::string& relative_path);
+
+// Returns the base URL for communicating with the local test server for
+// testing, running at the specified port number.
+GURL GetBaseUrlForTesting(int port);
+
+// Writes the |content| to the file at |file_path|. Returns true on success,
+// otherwise false.
+bool WriteStringToFile(const base::FilePath& file_path,
+ const std::string& content);
+
+// Creates a |size| byte file. The file is filled with random bytes so that
+// the test assertions can identify correct portion/position of the file is
+// used.
+// Returns true on success with the created file's |path| and |data|, otherwise
+// false.
+bool CreateFileOfSpecifiedSize(const base::FilePath& temp_dir,
+ size_t size,
+ base::FilePath* path,
+ std::string* data);
+
+// Loads a test JSON file as a base::Value, from a test file stored under
+// chrome/test/data.
+scoped_ptr<base::Value> LoadJSONFile(const std::string& relative_path);
+
+// Returns a HttpResponse created from the given file path.
+scoped_ptr<net::test_server::BasicHttpResponse> CreateHttpResponseFromFile(
+ const base::FilePath& file_path);
+
+// Handles a request for downloading a file. Reads a file from the test
+// directory and returns the content. Also, copies the |request| to the memory
+// pointed by |out_request|.
+// |base_url| must be set to the server's base url.
+scoped_ptr<net::test_server::HttpResponse> HandleDownloadFileRequest(
+ const GURL& base_url,
+ net::test_server::HttpRequest* out_request,
+ const net::test_server::HttpRequest& request);
+
+// Parses a value of Content-Range header, which looks like
+// "bytes <start_position>-<end_position>/<length>".
+// Returns true on success.
+bool ParseContentRangeHeader(const std::string& value,
+ int64* start_position,
+ int64* end_position,
+ int64* length);
+
+// Google API related code and Drive File System code work on asynchronous
+// architecture and return the results via callbacks.
+// Following code implements a callback to copy such results.
+// Here is how to use:
+//
+// // Prepare result storage.
+// ResultType1 result1;
+// ResultType2 result2;
+// :
+//
+// PerformAsynchronousTask(
+// param1, param2, ...,
+// CreateCopyResultCallback(&result1, &result2, ...));
+// base::RunLoop().RunUntilIdle(); // Run message loop to complete
+// // the async task.
+//
+// // Hereafter, we can write expectation with results.
+// EXPECT_EQ(expected_result1, result1);
+// EXPECT_EQ(expected_result2, result2);
+// :
+//
+// Note: The max arity of the supported function is 4 based on the usage.
+namespace internal {
+// Following helper templates are to support Chrome's move semantics.
+// Their goal is defining helper methods which are similar to:
+// void CopyResultCallback1(T1* out1, T1&& in1)
+// void CopyResultCallback2(T1* out1, T2* out2, T1&& in1, T2&& in2)
+// :
+// in C++11.
+
+// Declare if the type is movable or not. Currently limited to scoped_ptr only.
+// We can add more types upon the usage.
+template<typename T> struct IsMovable : base::false_type {};
+template<typename T, typename D>
+struct IsMovable<scoped_ptr<T, D> > : base::true_type {};
+
+// InType is const T& if |UseConstRef| is true, otherwise |T|.
+template<bool UseConstRef, typename T> struct InTypeHelper {
+ typedef const T& InType;
+};
+template<typename T> struct InTypeHelper<false, T> {
+ typedef T InType;
+};
+
+// Simulates the std::move function in C++11. We use pointer here for argument,
+// instead of rvalue reference.
+template<bool IsMovable, typename T> struct MoveHelper {
+ static const T& Move(const T* in) { return *in; }
+};
+template<typename T> struct MoveHelper<true, T> {
+ static T Move(T* in) { return in->Pass(); }
+};
+
+// Helper to handle Chrome's move semantics correctly.
+template<typename T>
+struct CopyResultCallbackHelper
+ // It is necessary to calculate the exact signature of callbacks we want
+ // to create here. In our case, as we use value-parameters for primitive
+ // types and movable types in the callback declaration.
+ // Thus the incoming type is as follows:
+ // 1) If the argument type |T| is class type but doesn't movable,
+ // |InType| is const T&.
+ // 2) Otherwise, |T| as is.
+ : InTypeHelper<
+ base::is_class<T>::value && !IsMovable<T>::value, // UseConstRef
+ T>,
+ MoveHelper<IsMovable<T>::value, T> {
+};
+
+// Copies the |in|'s value to |out|.
+template<typename T1>
+void CopyResultCallback(
+ T1* out,
+ typename CopyResultCallbackHelper<T1>::InType in) {
+ *out = CopyResultCallbackHelper<T1>::Move(&in);
+}
+
+// Copies the |in1|'s value to |out1|, and |in2|'s to |out2|.
+template<typename T1, typename T2>
+void CopyResultCallback(
+ T1* out1,
+ T2* out2,
+ typename CopyResultCallbackHelper<T1>::InType in1,
+ typename CopyResultCallbackHelper<T2>::InType in2) {
+ *out1 = CopyResultCallbackHelper<T1>::Move(&in1);
+ *out2 = CopyResultCallbackHelper<T2>::Move(&in2);
+}
+
+// Copies the |in1|'s value to |out1|, |in2|'s to |out2|, and |in3|'s to |out3|.
+template<typename T1, typename T2, typename T3>
+void CopyResultCallback(
+ T1* out1,
+ T2* out2,
+ T3* out3,
+ typename CopyResultCallbackHelper<T1>::InType in1,
+ typename CopyResultCallbackHelper<T2>::InType in2,
+ typename CopyResultCallbackHelper<T3>::InType in3) {
+ *out1 = CopyResultCallbackHelper<T1>::Move(&in1);
+ *out2 = CopyResultCallbackHelper<T2>::Move(&in2);
+ *out3 = CopyResultCallbackHelper<T3>::Move(&in3);
+}
+
+// Holds the pointers for output. This is introduced for the workaround of
+// the arity limitation of Callback.
+template<typename T1, typename T2, typename T3, typename T4>
+struct OutputParams {
+ OutputParams(T1* out1, T2* out2, T3* out3, T4* out4)
+ : out1(out1), out2(out2), out3(out3), out4(out4) {}
+ T1* out1;
+ T2* out2;
+ T3* out3;
+ T4* out4;
+};
+
+// Copies the |in1|'s value to |output->out1|, |in2|'s to |output->out2|,
+// and so on.
+template<typename T1, typename T2, typename T3, typename T4>
+void CopyResultCallback(
+ const OutputParams<T1, T2, T3, T4>& output,
+ typename CopyResultCallbackHelper<T1>::InType in1,
+ typename CopyResultCallbackHelper<T2>::InType in2,
+ typename CopyResultCallbackHelper<T3>::InType in3,
+ typename CopyResultCallbackHelper<T4>::InType in4) {
+ *output.out1 = CopyResultCallbackHelper<T1>::Move(&in1);
+ *output.out2 = CopyResultCallbackHelper<T2>::Move(&in2);
+ *output.out3 = CopyResultCallbackHelper<T3>::Move(&in3);
+ *output.out4 = CopyResultCallbackHelper<T4>::Move(&in4);
+}
+
+} // namespace internal
+
+template<typename T1>
+base::Callback<void(typename internal::CopyResultCallbackHelper<T1>::InType)>
+CreateCopyResultCallback(T1* out1) {
+ return base::Bind(&internal::CopyResultCallback<T1>, out1);
+}
+
+template<typename T1, typename T2>
+base::Callback<void(typename internal::CopyResultCallbackHelper<T1>::InType,
+ typename internal::CopyResultCallbackHelper<T2>::InType)>
+CreateCopyResultCallback(T1* out1, T2* out2) {
+ return base::Bind(&internal::CopyResultCallback<T1, T2>, out1, out2);
+}
+
+template<typename T1, typename T2, typename T3>
+base::Callback<void(typename internal::CopyResultCallbackHelper<T1>::InType,
+ typename internal::CopyResultCallbackHelper<T2>::InType,
+ typename internal::CopyResultCallbackHelper<T3>::InType)>
+CreateCopyResultCallback(T1* out1, T2* out2, T3* out3) {
+ return base::Bind(
+ &internal::CopyResultCallback<T1, T2, T3>, out1, out2, out3);
+}
+
+template<typename T1, typename T2, typename T3, typename T4>
+base::Callback<void(typename internal::CopyResultCallbackHelper<T1>::InType,
+ typename internal::CopyResultCallbackHelper<T2>::InType,
+ typename internal::CopyResultCallbackHelper<T3>::InType,
+ typename internal::CopyResultCallbackHelper<T4>::InType)>
+CreateCopyResultCallback(T1* out1, T2* out2, T3* out3, T4* out4) {
+ return base::Bind(
+ &internal::CopyResultCallback<T1, T2, T3, T4>,
+ internal::OutputParams<T1, T2, T3, T4>(out1, out2, out3, out4));
+}
+
+typedef std::pair<int64, int64> ProgressInfo;
+
+// Helper utility for recording the results via ProgressCallback.
+void AppendProgressCallbackResult(std::vector<ProgressInfo>* progress_values,
+ int64 progress,
+ int64 total);
+
+// Helper utility for recording the content via GetContentCallback.
+class TestGetContentCallback {
+ public:
+ TestGetContentCallback();
+ ~TestGetContentCallback();
+
+ const GetContentCallback& callback() const { return callback_; }
+ const ScopedVector<std::string>& data() const { return data_; }
+ ScopedVector<std::string>* mutable_data() { return &data_; }
+ std::string GetConcatenatedData() const;
+
+ private:
+ void OnGetContent(google_apis::GDataErrorCode error,
+ scoped_ptr<std::string> data);
+
+ const GetContentCallback callback_;
+ ScopedVector<std::string> data_;
+
+ DISALLOW_COPY_AND_ASSIGN(TestGetContentCallback);
+};
+
+} // namespace test_util
+} // namespace google_apis
+
+#endif // GOOGLE_APIS_DRIVE_TEST_UTIL_H_
diff --git a/chromium/google_apis/drive/time_util.cc b/chromium/google_apis/drive/time_util.cc
new file mode 100644
index 00000000000..6ac55e944c6
--- /dev/null
+++ b/chromium/google_apis/drive/time_util.cc
@@ -0,0 +1,170 @@
+// Copyright (c) 2012 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "google_apis/drive/time_util.h"
+
+#include <string>
+#include <vector>
+
+#include "base/strings/string_number_conversions.h"
+#include "base/strings/string_util.h"
+#include "base/strings/stringprintf.h"
+#include "base/time/time.h"
+
+namespace google_apis {
+namespace util {
+
+namespace {
+
+const char kNullTimeString[] = "null";
+
+bool ParseTimezone(const base::StringPiece& timezone,
+ bool ahead,
+ int* out_offset_to_utc_in_minutes) {
+ DCHECK(out_offset_to_utc_in_minutes);
+
+ std::vector<base::StringPiece> parts;
+ int num_of_token = Tokenize(timezone, ":", &parts);
+
+ int hour = 0;
+ if (!base::StringToInt(parts[0], &hour))
+ return false;
+
+ int minute = 0;
+ if (num_of_token > 1 && !base::StringToInt(parts[1], &minute))
+ return false;
+
+ *out_offset_to_utc_in_minutes = (hour * 60 + minute) * (ahead ? +1 : -1);
+ return true;
+}
+
+} // namespace
+
+bool GetTimeFromString(const base::StringPiece& raw_value,
+ base::Time* parsed_time) {
+ base::StringPiece date;
+ base::StringPiece time_and_tz;
+ base::StringPiece time;
+ base::Time::Exploded exploded = {0};
+ bool has_timezone = false;
+ int offset_to_utc_in_minutes = 0;
+
+ // Splits the string into "date" part and "time" part.
+ {
+ std::vector<base::StringPiece> parts;
+ if (Tokenize(raw_value, "T", &parts) != 2)
+ return false;
+ date = parts[0];
+ time_and_tz = parts[1];
+ }
+
+ // Parses timezone suffix on the time part if available.
+ {
+ std::vector<base::StringPiece> parts;
+ if (time_and_tz[time_and_tz.size() - 1] == 'Z') {
+ // Timezone is 'Z' (UTC)
+ has_timezone = true;
+ offset_to_utc_in_minutes = 0;
+ time = time_and_tz;
+ time.remove_suffix(1);
+ } else if (Tokenize(time_and_tz, "+", &parts) == 2) {
+ // Timezone is "+hh:mm" format
+ if (!ParseTimezone(parts[1], true, &offset_to_utc_in_minutes))
+ return false;
+ has_timezone = true;
+ time = parts[0];
+ } else if (Tokenize(time_and_tz, "-", &parts) == 2) {
+ // Timezone is "-hh:mm" format
+ if (!ParseTimezone(parts[1], false, &offset_to_utc_in_minutes))
+ return false;
+ has_timezone = true;
+ time = parts[0];
+ } else {
+ // No timezone (uses local timezone)
+ time = time_and_tz;
+ }
+ }
+
+ // Parses the date part.
+ {
+ std::vector<base::StringPiece> parts;
+ if (Tokenize(date, "-", &parts) != 3)
+ return false;
+
+ if (!base::StringToInt(parts[0], &exploded.year) ||
+ !base::StringToInt(parts[1], &exploded.month) ||
+ !base::StringToInt(parts[2], &exploded.day_of_month)) {
+ return false;
+ }
+ }
+
+ // Parses the time part.
+ {
+ std::vector<base::StringPiece> parts;
+ int num_of_token = Tokenize(time, ":", &parts);
+ if (num_of_token != 3)
+ return false;
+
+ if (!base::StringToInt(parts[0], &exploded.hour) ||
+ !base::StringToInt(parts[1], &exploded.minute)) {
+ return false;
+ }
+
+ std::vector<base::StringPiece> seconds_parts;
+ int num_of_seconds_token = Tokenize(parts[2], ".", &seconds_parts);
+ if (num_of_seconds_token >= 3)
+ return false;
+
+ if (!base::StringToInt(seconds_parts[0], &exploded.second))
+ return false;
+
+ // Only accept milli-seconds (3-digits).
+ if (num_of_seconds_token > 1 &&
+ seconds_parts[1].length() == 3 &&
+ !base::StringToInt(seconds_parts[1], &exploded.millisecond)) {
+ return false;
+ }
+ }
+
+ exploded.day_of_week = 0;
+ if (!exploded.HasValidValues())
+ return false;
+
+ if (has_timezone) {
+ *parsed_time = base::Time::FromUTCExploded(exploded);
+ if (offset_to_utc_in_minutes != 0)
+ *parsed_time -= base::TimeDelta::FromMinutes(offset_to_utc_in_minutes);
+ } else {
+ *parsed_time = base::Time::FromLocalExploded(exploded);
+ }
+
+ return true;
+}
+
+std::string FormatTimeAsString(const base::Time& time) {
+ if (time.is_null())
+ return kNullTimeString;
+
+ base::Time::Exploded exploded;
+ time.UTCExplode(&exploded);
+ return base::StringPrintf(
+ "%04d-%02d-%02dT%02d:%02d:%02d.%03dZ",
+ exploded.year, exploded.month, exploded.day_of_month,
+ exploded.hour, exploded.minute, exploded.second, exploded.millisecond);
+}
+
+std::string FormatTimeAsStringLocaltime(const base::Time& time) {
+ if (time.is_null())
+ return kNullTimeString;
+
+ base::Time::Exploded exploded;
+ time.LocalExplode(&exploded);
+ return base::StringPrintf(
+ "%04d-%02d-%02dT%02d:%02d:%02d.%03d",
+ exploded.year, exploded.month, exploded.day_of_month,
+ exploded.hour, exploded.minute, exploded.second, exploded.millisecond);
+}
+
+} // namespace util
+} // namespace google_apis
diff --git a/chromium/google_apis/drive/time_util.h b/chromium/google_apis/drive/time_util.h
new file mode 100644
index 00000000000..f281d800f83
--- /dev/null
+++ b/chromium/google_apis/drive/time_util.h
@@ -0,0 +1,35 @@
+// Copyright (c) 2012 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef GOOGLE_APIS_DRIVE_TIME_UTIL_H_
+#define GOOGLE_APIS_DRIVE_TIME_UTIL_H_
+
+#include <string>
+
+#include "base/strings/string_piece.h"
+
+namespace base {
+class Time;
+} // namespace base
+
+namespace google_apis {
+namespace util {
+
+// Parses an RFC 3339 date/time into a base::Time, returning true on success.
+// The time string must be in the format "yyyy-mm-ddThh:mm:ss.dddTZ" (TZ is
+// either '+hh:mm', '-hh:mm', 'Z' (representing UTC), or an empty string).
+bool GetTimeFromString(const base::StringPiece& raw_value, base::Time* time);
+
+// Formats a base::Time as an RFC 3339 date/time (in UTC).
+// If |time| is null, returns "null".
+std::string FormatTimeAsString(const base::Time& time);
+
+// Formats a base::Time as an RFC 3339 date/time (in localtime).
+// If |time| is null, returns "null".
+std::string FormatTimeAsStringLocaltime(const base::Time& time);
+
+} // namespace util
+} // namespace google_apis
+
+#endif // GOOGLE_APIS_DRIVE_TIME_UTIL_H_
diff --git a/chromium/google_apis/drive/time_util_unittest.cc b/chromium/google_apis/drive/time_util_unittest.cc
new file mode 100644
index 00000000000..6d3abf6717c
--- /dev/null
+++ b/chromium/google_apis/drive/time_util_unittest.cc
@@ -0,0 +1,96 @@
+// Copyright (c) 2012 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "google_apis/drive/time_util.h"
+
+#include "base/i18n/time_formatting.h"
+#include "base/strings/utf_string_conversions.h"
+#include "base/time/time.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+namespace google_apis {
+namespace util {
+namespace {
+
+std::string FormatTime(const base::Time& time) {
+ return base::UTF16ToUTF8(base::TimeFormatShortDateAndTime(time));
+}
+
+} // namespace
+
+TEST(TimeUtilTest, GetTimeFromStringLocalTimezone) {
+ // Creates local time objects from exploded structure.
+ base::Time::Exploded exploded = {2013, 1, 0, 15, 17, 11, 35, 374};
+ base::Time local_time = base::Time::FromLocalExploded(exploded);
+
+ // Creates local time object, parsing time string. Note that if there is
+ // not timezone suffix, GetTimeFromString() will handle this as local time
+ // with FromLocalExploded().
+ base::Time test_time;
+ ASSERT_TRUE(GetTimeFromString("2013-01-15T17:11:35.374", &test_time));
+
+ // Compare the time objects.
+ EXPECT_EQ(local_time, test_time);
+}
+
+TEST(TimeUtilTest, GetTimeFromStringNonTrivialTimezones) {
+ base::Time target_time;
+ base::Time test_time;
+ // Creates the target time.
+ EXPECT_TRUE(GetTimeFromString("2012-07-14T01:03:21.151Z", &target_time));
+
+ // Tests positive offset (hour only).
+ EXPECT_TRUE(GetTimeFromString("2012-07-14T02:03:21.151+01", &test_time));
+ EXPECT_EQ(FormatTime(target_time), FormatTime(test_time));
+
+ // Tests positive offset (hour and minutes).
+ EXPECT_TRUE(GetTimeFromString("2012-07-14T07:33:21.151+06:30", &test_time));
+ EXPECT_EQ(FormatTime(target_time), FormatTime(test_time));
+
+ // Tests negative offset.
+ EXPECT_TRUE(GetTimeFromString("2012-07-13T18:33:21.151-06:30", &test_time));
+ EXPECT_EQ(FormatTime(target_time), FormatTime(test_time));
+}
+
+TEST(TimeUtilTest, GetTimeFromStringBasic) {
+ base::Time test_time;
+
+ // Test that the special timezone "Z" (UTC) is handled.
+ base::Time::Exploded target_time1 = {2005, 1, 0, 7, 8, 2, 0, 0};
+ EXPECT_TRUE(GetTimeFromString("2005-01-07T08:02:00Z", &test_time));
+ EXPECT_EQ(FormatTime(base::Time::FromUTCExploded(target_time1)),
+ FormatTime(test_time));
+
+ // Test that a simple timezone "-08:00" is handled
+ // 17:57 - 8 hours = 09:57
+ base::Time::Exploded target_time2 = {2005, 8, 0, 9, 17, 57, 0, 0};
+ EXPECT_TRUE(GetTimeFromString("2005-08-09T09:57:00-08:00", &test_time));
+ EXPECT_EQ(FormatTime(base::Time::FromUTCExploded(target_time2)),
+ FormatTime(test_time));
+
+ // Test that milliseconds (.123) are handled.
+ base::Time::Exploded target_time3 = {2005, 1, 0, 7, 8, 2, 0, 123};
+ EXPECT_TRUE(GetTimeFromString("2005-01-07T08:02:00.123Z", &test_time));
+ EXPECT_EQ(FormatTime(base::Time::FromUTCExploded(target_time3)),
+ FormatTime(test_time));
+}
+
+TEST(TimeUtilTest, FormatTimeAsString) {
+ base::Time::Exploded exploded_time = {2012, 7, 0, 19, 15, 59, 13, 123};
+ base::Time time = base::Time::FromUTCExploded(exploded_time);
+ EXPECT_EQ("2012-07-19T15:59:13.123Z", FormatTimeAsString(time));
+
+ EXPECT_EQ("null", FormatTimeAsString(base::Time()));
+}
+
+TEST(TimeUtilTest, FormatTimeAsStringLocalTime) {
+ base::Time::Exploded exploded_time = {2012, 7, 0, 19, 15, 59, 13, 123};
+ base::Time time = base::Time::FromLocalExploded(exploded_time);
+ EXPECT_EQ("2012-07-19T15:59:13.123", FormatTimeAsStringLocaltime(time));
+
+ EXPECT_EQ("null", FormatTimeAsStringLocaltime(base::Time()));
+}
+
+} // namespace util
+} // namespace google_apis
diff --git a/chromium/google_apis/gaia/fake_gaia.cc b/chromium/google_apis/gaia/fake_gaia.cc
index 9f4a8122a22..322ed54ff9f 100644
--- a/chromium/google_apis/gaia/fake_gaia.cc
+++ b/chromium/google_apis/gaia/fake_gaia.cc
@@ -10,6 +10,8 @@
#include "base/json/json_writer.h"
#include "base/logging.h"
#include "base/path_service.h"
+#include "base/strings/string_number_conversions.h"
+#include "base/strings/string_split.h"
#include "base/strings/string_util.h"
#include "base/values.h"
#include "google_apis/gaia/gaia_urls.h"
@@ -65,9 +67,13 @@ scoped_ptr<HttpResponse> FakeGaia::HandleRequest(const HttpRequest& request) {
http_response->set_content_type("text/html");
} else if (request_path == gaia_urls->oauth2_token_url().path()) {
std::string refresh_token;
+ std::string client_id;
+ std::string scope;
const AccessTokenInfo* token_info = NULL;
+ GetQueryParameter(request.content, "scope", &scope);
if (GetQueryParameter(request.content, "refresh_token", &refresh_token) &&
- (token_info = GetAccessTokenInfo(refresh_token))) {
+ GetQueryParameter(request.content, "client_id", &client_id) &&
+ (token_info = GetAccessTokenInfo(refresh_token, client_id, scope))) {
base::DictionaryValue response_dict;
response_dict.SetString("access_token", token_info->token);
response_dict.SetInteger("expires_in", 3600);
@@ -104,6 +110,30 @@ scoped_ptr<HttpResponse> FakeGaia::HandleRequest(const HttpRequest& request) {
} else {
http_response->set_code(net::HTTP_BAD_REQUEST);
}
+ } else if (request_path == gaia_urls->oauth2_issue_token_url().path()) {
+ std::string access_token;
+ std::map<std::string, std::string>::const_iterator auth_header_entry =
+ request.headers.find("Authorization");
+ if (auth_header_entry != request.headers.end()) {
+ if (StartsWithASCII(auth_header_entry->second, "Bearer ", true))
+ access_token = auth_header_entry->second.substr(7);
+ }
+
+ std::string scope;
+ std::string client_id;
+ const AccessTokenInfo* token_info = NULL;
+ if (GetQueryParameter(request.content, "scope", &scope) &&
+ GetQueryParameter(request.content, "client_id", &client_id) &&
+ (token_info = GetAccessTokenInfo(access_token, client_id, scope))) {
+ base::DictionaryValue response_dict;
+ response_dict.SetString("issueAdvice", "auto");
+ response_dict.SetString("expiresIn",
+ base::IntToString(token_info->expires_in));
+ response_dict.SetString("token", token_info->token);
+ FormatJSONResponse(response_dict, http_response.get());
+ } else {
+ http_response->set_code(net::HTTP_BAD_REQUEST);
+ }
} else {
// Request not understood.
return scoped_ptr<HttpResponse>();
@@ -112,9 +142,9 @@ scoped_ptr<HttpResponse> FakeGaia::HandleRequest(const HttpRequest& request) {
return http_response.PassAs<HttpResponse>();
}
-void FakeGaia::IssueOAuthToken(const std::string& refresh_token,
+void FakeGaia::IssueOAuthToken(const std::string& auth_token,
const AccessTokenInfo& token_info) {
- access_token_info_map_[refresh_token] = token_info;
+ access_token_info_map_.insert(std::make_pair(auth_token, token_info));
}
void FakeGaia::FormatJSONResponse(const base::DictionaryValue& response_dict,
@@ -126,10 +156,27 @@ void FakeGaia::FormatJSONResponse(const base::DictionaryValue& response_dict,
}
const FakeGaia::AccessTokenInfo* FakeGaia::GetAccessTokenInfo(
- const std::string& refresh_token) const {
- AccessTokenInfoMap::const_iterator entry =
- access_token_info_map_.find(refresh_token);
- return entry == access_token_info_map_.end() ? NULL : &entry->second;
+ const std::string& auth_token,
+ const std::string& client_id,
+ const std::string& scope_string) const {
+ if (auth_token.empty() || client_id.empty())
+ return NULL;
+
+ std::vector<std::string> scope_list;
+ base::SplitString(scope_string, ' ', &scope_list);
+ ScopeSet scopes(scope_list.begin(), scope_list.end());
+
+ for (AccessTokenInfoMap::const_iterator entry(
+ access_token_info_map_.lower_bound(auth_token));
+ entry != access_token_info_map_.upper_bound(auth_token);
+ ++entry) {
+ if (entry->second.audience == client_id &&
+ (scope_string.empty() || entry->second.scopes == scopes)) {
+ return &(entry->second);
+ }
+ }
+
+ return NULL;
}
// static
diff --git a/chromium/google_apis/gaia/fake_gaia.h b/chromium/google_apis/gaia/fake_gaia.h
index 24f6891c420..de4201f7337 100644
--- a/chromium/google_apis/gaia/fake_gaia.h
+++ b/chromium/google_apis/gaia/fake_gaia.h
@@ -55,21 +55,27 @@ class FakeGaia {
const net::test_server::HttpRequest& request);
// Configures an OAuth2 token that'll be returned when a client requests an
- // access token for the given refresh token.
- void IssueOAuthToken(const std::string& refresh_token,
+ // access token for the given auth token, which can be a refresh token or an
+ // login-scoped access token for the token minting endpoint. Note that the
+ // scope and audience requested by the client need to match the token_info.
+ void IssueOAuthToken(const std::string& auth_token,
const AccessTokenInfo& token_info);
private:
- typedef std::map<std::string, AccessTokenInfo> AccessTokenInfoMap;
+ typedef std::multimap<std::string, AccessTokenInfo> AccessTokenInfoMap;
// Formats a JSON response with the data in |response_dict|.
void FormatJSONResponse(const base::DictionaryValue& response_dict,
net::test_server::BasicHttpResponse* http_response);
- // Returns the access token associated with |refresh_token| or NULL if not
- // found.
- const AccessTokenInfo* GetAccessTokenInfo(
- const std::string& refresh_token) const;
+ // Returns the access token associated with |auth_token| that matches the
+ // given |client_id| and |scope_string|. If |scope_string| is empty, the first
+ // token satisfying the other criteria is returned. Returns NULL if no token
+ // matches.
+ const AccessTokenInfo* GetAccessTokenInfo(const std::string& auth_token,
+ const std::string& client_id,
+ const std::string& scope_string)
+ const;
// Extracts the parameter named |key| from |query| and places it in |value|.
// Returns false if no parameter is found.
diff --git a/chromium/google_apis/gaia/gaia_auth_consumer.h b/chromium/google_apis/gaia/gaia_auth_consumer.h
index 403033622b2..d5ab306c355 100644
--- a/chromium/google_apis/gaia/gaia_auth_consumer.h
+++ b/chromium/google_apis/gaia/gaia_auth_consumer.h
@@ -82,6 +82,9 @@ class GaiaAuthConsumer {
virtual void OnMergeSessionSuccess(const std::string& data) {}
virtual void OnMergeSessionFailure(const GoogleServiceAuthError& error) {}
+
+ virtual void OnListAccountsSuccess(const std::string& data) {}
+ virtual void OnListAccountsFailure(const GoogleServiceAuthError& error) {}
};
#endif // GOOGLE_APIS_GAIA_GAIA_AUTH_CONSUMER_H_
diff --git a/chromium/google_apis/gaia/gaia_auth_fetcher.cc b/chromium/google_apis/gaia/gaia_auth_fetcher.cc
index f0e26445a52..c5254f4235e 100644
--- a/chromium/google_apis/gaia/gaia_auth_fetcher.cc
+++ b/chromium/google_apis/gaia/gaia_auth_fetcher.cc
@@ -182,6 +182,7 @@ GaiaAuthFetcher::GaiaAuthFetcher(GaiaAuthConsumer* consumer,
uberauth_token_gurl_(GaiaUrls::GetInstance()->oauth1_login_url().Resolve(
base::StringPrintf(kUberAuthTokenURLFormat, source.c_str()))),
oauth_login_gurl_(GaiaUrls::GetInstance()->oauth1_login_url()),
+ list_accounts_gurl_(GaiaUrls::GetInstance()->list_accounts_url()),
client_login_to_oauth2_gurl_(
GaiaUrls::GetInstance()->client_login_to_oauth2_url()),
fetch_pending_(false) {}
@@ -219,8 +220,8 @@ net::URLFetcher* GaiaAuthFetcher::CreateGaiaFetcher(
// The Gaia token exchange requests do not require any cookie-based
// identification as part of requests. We suppress sending any cookies to
// maintain a separation between the user's browsing and Chrome's internal
- // services. Where such mixing is desired (MergeSession), it will be done
- // explicitly.
+ // services. Where such mixing is desired (MergeSession or OAuthLogin), it
+ // will be done explicitly.
to_return->SetLoadFlags(load_flags);
// Fetchers are sometimes cancelled because a network change was detected,
@@ -636,7 +637,7 @@ void GaiaAuthFetcher::StartTokenFetchForUberAuthExchange(
std::string(),
authentication_header,
uberauth_token_gurl_,
- kLoadFlagsIgnoreCookies,
+ net::LOAD_NORMAL,
this));
fetch_pending_ = true;
fetcher_->Start();
@@ -653,7 +654,20 @@ void GaiaAuthFetcher::StartOAuthLogin(const std::string& access_token,
request_body_,
authentication_header,
oauth_login_gurl_,
- kLoadFlagsIgnoreCookies,
+ net::LOAD_NORMAL,
+ this));
+ fetch_pending_ = true;
+ fetcher_->Start();
+}
+
+void GaiaAuthFetcher::StartListAccounts() {
+ DCHECK(!fetch_pending_) << "Tried to fetch two things at once!";
+
+ fetcher_.reset(CreateGaiaFetcher(getter_,
+ " ", // To force an HTTP POST.
+ "Origin: https://www.google.com",
+ list_accounts_gurl_,
+ net::LOAD_NORMAL,
this));
fetch_pending_ = true;
fetcher_->Start();
@@ -844,6 +858,16 @@ void GaiaAuthFetcher::OnOAuth2RevokeTokenFetched(
consumer_->OnOAuth2RevokeTokenCompleted();
}
+void GaiaAuthFetcher::OnListAccountsFetched(const std::string& data,
+ const net::URLRequestStatus& status,
+ int response_code) {
+ if (status.is_success() && response_code == net::HTTP_OK) {
+ consumer_->OnListAccountsSuccess(data);
+ } else {
+ consumer_->OnListAccountsFailure(GenerateAuthError(data, status));
+ }
+}
+
void GaiaAuthFetcher::OnGetUserInfoFetched(
const std::string& data,
const net::URLRequestStatus& status,
@@ -937,6 +961,8 @@ void GaiaAuthFetcher::OnURLFetchComplete(const net::URLFetcher* source) {
OnOAuthLoginFetched(data, status, response_code);
} else if (url == oauth2_revoke_gurl_) {
OnOAuth2RevokeTokenFetched(data, status, response_code);
+ } else if (url == list_accounts_gurl_) {
+ OnListAccountsFetched(data, status, response_code);
} else {
NOTREACHED();
}
diff --git a/chromium/google_apis/gaia/gaia_auth_fetcher.h b/chromium/google_apis/gaia/gaia_auth_fetcher.h
index 5021a534ce3..0864f187cfb 100644
--- a/chromium/google_apis/gaia/gaia_auth_fetcher.h
+++ b/chromium/google_apis/gaia/gaia_auth_fetcher.h
@@ -158,6 +158,9 @@ class GaiaAuthFetcher : public net::URLFetcherDelegate {
void StartOAuthLogin(const std::string& access_token,
const std::string& service);
+ // Starts a request to list the accounts in the GAIA cookie.
+ void StartListAccounts();
+
// Implementation of net::URLFetcherDelegate
virtual void OnURLFetchComplete(const net::URLFetcher* source) OVERRIDE;
@@ -253,6 +256,10 @@ class GaiaAuthFetcher : public net::URLFetcherDelegate {
const net::URLRequestStatus& status,
int response_code);
+ void OnListAccountsFetched(const std::string& data,
+ const net::URLRequestStatus& status,
+ int response_code);
+
void OnGetUserInfoFetched(const std::string& data,
const net::URLRequestStatus& status,
int response_code);
@@ -359,6 +366,7 @@ class GaiaAuthFetcher : public net::URLFetcherDelegate {
const GURL merge_session_gurl_;
const GURL uberauth_token_gurl_;
const GURL oauth_login_gurl_;
+ const GURL list_accounts_gurl_;
// While a fetch is going on:
scoped_ptr<net::URLFetcher> fetcher_;
diff --git a/chromium/google_apis/gaia/gaia_auth_fetcher_unittest.cc b/chromium/google_apis/gaia/gaia_auth_fetcher_unittest.cc
index f554b5ed855..a12fb97b220 100644
--- a/chromium/google_apis/gaia/gaia_auth_fetcher_unittest.cc
+++ b/chromium/google_apis/gaia/gaia_auth_fetcher_unittest.cc
@@ -30,33 +30,21 @@
using ::testing::Invoke;
using ::testing::_;
-namespace {
-static const char kGetAuthCodeValidCookie[] =
+const char kGetAuthCodeValidCookie[] =
"oauth_code=test-code; Path=/test; Secure; HttpOnly";
-static const char kGetAuthCodeCookieNoSecure[] =
+const char kGetAuthCodeCookieNoSecure[] =
"oauth_code=test-code; Path=/test; HttpOnly";
-static const char kGetAuthCodeCookieNoHttpOnly[] =
+const char kGetAuthCodeCookieNoHttpOnly[] =
"oauth_code=test-code; Path=/test; Secure";
-static const char kGetAuthCodeCookieNoOAuthCode[] =
+const char kGetAuthCodeCookieNoOAuthCode[] =
"Path=/test; Secure; HttpOnly";
-static const char kGetTokenPairValidResponse[] =
+const char kGetTokenPairValidResponse[] =
"{"
" \"refresh_token\": \"rt1\","
" \"access_token\": \"at1\","
" \"expires_in\": 3600,"
" \"token_type\": \"Bearer\""
"}";
-static const char kClientOAuthValidResponse[] =
- "{"
- " \"oauth2\": {"
- " \"refresh_token\": \"rt1\","
- " \"access_token\": \"at1\","
- " \"expires_in\": 3600,"
- " \"token_type\": \"Bearer\""
- " }"
- "}";
-
-} // namespace
MockFetcher::MockFetcher(bool success,
const GURL& url,
@@ -199,6 +187,7 @@ class MockGaiaConsumer : public GaiaAuthConsumer {
const GoogleServiceAuthError& error));
MOCK_METHOD1(OnUberAuthTokenFailure, void(
const GoogleServiceAuthError& error));
+ MOCK_METHOD1(OnListAccountsSuccess, void(const std::string& data));
};
#if defined(OS_WIN)
@@ -801,3 +790,17 @@ TEST_F(GaiaAuthFetcherTest, StartOAuthLogin) {
net::URLFetcher::GET, &auth);
auth.OnURLFetchComplete(&mock_fetcher);
}
+
+TEST_F(GaiaAuthFetcherTest, ListAccounts) {
+ std::string data("[\"gaia.l.a.r\", ["
+ "[\"gaia.l.a\", 1, \"First Last\", \"user@gmail.com\", "
+ "\"//googleusercontent.com/A/B/C/D/photo.jpg\", 1, 1, 0]]]");
+ MockGaiaConsumer consumer;
+ EXPECT_CALL(consumer, OnListAccountsSuccess(data)).Times(1);
+
+ GaiaAuthFetcher auth(&consumer, std::string(), GetRequestContext());
+ net::URLRequestStatus status(net::URLRequestStatus::SUCCESS, 0);
+ MockFetcher mock_fetcher(GaiaUrls::GetInstance()->list_accounts_url(),
+ status, net::HTTP_OK, cookies_, data, net::URLFetcher::GET, &auth);
+ auth.OnURLFetchComplete(&mock_fetcher);
+}
diff --git a/chromium/google_apis/gaia/gaia_auth_util.cc b/chromium/google_apis/gaia/gaia_auth_util.cc
index a8cad86408c..f8f95c14f5b 100644
--- a/chromium/google_apis/gaia/gaia_auth_util.cc
+++ b/chromium/google_apis/gaia/gaia_auth_util.cc
@@ -4,11 +4,11 @@
#include "google_apis/gaia/gaia_auth_util.h"
-#include <vector>
-
+#include "base/json/json_reader.h"
#include "base/logging.h"
#include "base/strings/string_split.h"
#include "base/strings/string_util.h"
+#include "base/values.h"
#include "google_apis/gaia/gaia_urls.h"
#include "url/gurl.h"
@@ -25,7 +25,7 @@ std::string CanonicalizeEmail(const std::string& email_address) {
if (parts.size() != 2U)
NOTREACHED() << "expecting exactly one @, but got " << parts.size();
else if (parts[1] == kGmailDomain) // only strip '.' for gmail accounts.
- RemoveChars(parts[0], ".", &parts[0]);
+ base::RemoveChars(parts[0], ".", &parts[0]);
std::string new_email = StringToLowerASCII(JoinString(parts, at));
VLOG(1) << "Canonicalized " << email_address << " to " << new_email;
return new_email;
@@ -72,4 +72,36 @@ bool IsGaiaSignonRealm(const GURL& url) {
return url == GaiaUrls::GetInstance()->gaia_url();
}
+
+std::vector<std::string> ParseListAccountsData(const std::string& data) {
+ std::vector<std::string> account_ids;
+
+ // Parse returned data and make sure we have data.
+ scoped_ptr<base::Value> value(base::JSONReader::Read(data));
+ if (!value)
+ return account_ids;
+
+ base::ListValue* list;
+ if (!value->GetAsList(&list) || list->GetSize() < 2)
+ return account_ids;
+
+ // Get list of account info.
+ base::ListValue* accounts;
+ if (!list->GetList(1, &accounts) || accounts == NULL)
+ return account_ids;
+
+ // Build a vector of accounts from the cookie. Order is important: the first
+ // account in the list is the primary account.
+ for (size_t i = 0; i < accounts->GetSize(); ++i) {
+ base::ListValue* account;
+ if (accounts->GetList(i, &account) && account != NULL) {
+ std::string email;
+ if (account->GetString(3, &email) && !email.empty())
+ account_ids.push_back(email);
+ }
+ }
+
+ return account_ids;
+}
+
} // namespace gaia
diff --git a/chromium/google_apis/gaia/gaia_auth_util.h b/chromium/google_apis/gaia/gaia_auth_util.h
index 35e834b3629..354d116e7a5 100644
--- a/chromium/google_apis/gaia/gaia_auth_util.h
+++ b/chromium/google_apis/gaia/gaia_auth_util.h
@@ -6,6 +6,7 @@
#define GOOGLE_APIS_GAIA_GAIA_AUTH_UTIL_H_
#include <string>
+#include <vector>
class GURL;
@@ -34,6 +35,10 @@ std::string ExtractDomainName(const std::string& email);
bool IsGaiaSignonRealm(const GURL& url);
+// Parses JSON data returned by /ListAccounts call, returns vector of
+// accounts (email addresses).
+std::vector<std::string> ParseListAccountsData(const std::string& data);
+
} // namespace gaia
#endif // GOOGLE_APIS_GAIA_GAIA_AUTH_UTIL_H_
diff --git a/chromium/google_apis/gaia/gaia_auth_util_unittest.cc b/chromium/google_apis/gaia/gaia_auth_util_unittest.cc
index 4fe58a3bba4..ebe8f876abe 100644
--- a/chromium/google_apis/gaia/gaia_auth_util_unittest.cc
+++ b/chromium/google_apis/gaia/gaia_auth_util_unittest.cc
@@ -108,4 +108,38 @@ TEST(GaiaAuthUtilTest, IsGaiaSignonRealm) {
EXPECT_FALSE(IsGaiaSignonRealm(GURL("https://www.example.com/")));
}
+TEST(GaiaAuthUtilTest, ParseListAccountsData) {
+ std::vector<std::string> accounts;
+ accounts = ParseListAccountsData("");
+ ASSERT_EQ(0u, accounts.size());
+
+ accounts = ParseListAccountsData("1");
+ ASSERT_EQ(0u, accounts.size());
+
+ accounts = ParseListAccountsData("[]");
+ ASSERT_EQ(0u, accounts.size());
+
+ accounts = ParseListAccountsData("[\"foo\", \"bar\"]");
+ ASSERT_EQ(0u, accounts.size());
+
+ accounts = ParseListAccountsData("[\"foo\", []]");
+ ASSERT_EQ(0u, accounts.size());
+
+ accounts = ParseListAccountsData(
+ "[\"foo\", [[\"bar\", 0, \"name\", 0, \"photo\", 0, 0, 0]]]");
+ ASSERT_EQ(0u, accounts.size());
+
+ accounts = ParseListAccountsData(
+ "[\"foo\", [[\"bar\", 0, \"name\", \"email\", \"photo\", 0, 0, 0]]]");
+ ASSERT_EQ(1u, accounts.size());
+ ASSERT_EQ("email", accounts[0]);
+
+ accounts = ParseListAccountsData(
+ "[\"foo\", [[\"bar1\", 0, \"name1\", \"email1\", \"photo1\", 0, 0, 0], "
+ "[\"bar2\", 0, \"name2\", \"email2\", \"photo2\", 0, 0, 0]]]");
+ ASSERT_EQ(2u, accounts.size());
+ ASSERT_EQ("email1", accounts[0]);
+ ASSERT_EQ("email2", accounts[1]);
+}
+
} // namespace gaia
diff --git a/chromium/google_apis/gaia/gaia_oauth_client.cc b/chromium/google_apis/gaia/gaia_oauth_client.cc
index 86064d9bc62..8c6e1807369 100644
--- a/chromium/google_apis/gaia/gaia_oauth_client.cc
+++ b/chromium/google_apis/gaia/gaia_oauth_client.cc
@@ -11,6 +11,7 @@
#include "base/values.h"
#include "google_apis/gaia/gaia_urls.h"
#include "net/base/escape.h"
+#include "net/base/load_flags.h"
#include "net/http/http_status_code.h"
#include "net/url_request/url_fetcher.h"
#include "net/url_request/url_fetcher_delegate.h"
@@ -52,6 +53,9 @@ class GaiaOAuthClient::Core
void GetUserEmail(const std::string& oauth_access_token,
int max_retries,
Delegate* delegate);
+ void GetUserId(const std::string& oauth_access_token,
+ int max_retries,
+ Delegate* delegate);
void GetTokenInfo(const std::string& oauth_access_token,
int max_retries,
Delegate* delegate);
@@ -67,11 +71,15 @@ class GaiaOAuthClient::Core
TOKENS_FROM_AUTH_CODE,
REFRESH_TOKEN,
TOKEN_INFO,
- USER_INFO,
+ USER_EMAIL,
+ USER_ID,
};
virtual ~Core() {}
+ void GetUserInfo(const std::string& oauth_access_token,
+ int max_retries,
+ Delegate* delegate);
void MakeGaiaRequest(const GURL& url,
const std::string& post_body,
int max_retries,
@@ -136,7 +144,22 @@ void GaiaOAuthClient::Core::GetUserEmail(const std::string& oauth_access_token,
Delegate* delegate) {
DCHECK_EQ(request_type_, NO_PENDING_REQUEST);
DCHECK(!request_.get());
- request_type_ = USER_INFO;
+ request_type_ = USER_EMAIL;
+ GetUserInfo(oauth_access_token, max_retries, delegate);
+}
+
+void GaiaOAuthClient::Core::GetUserId(const std::string& oauth_access_token,
+ int max_retries,
+ Delegate* delegate) {
+ DCHECK_EQ(request_type_, NO_PENDING_REQUEST);
+ DCHECK(!request_.get());
+ request_type_ = USER_ID;
+ GetUserInfo(oauth_access_token, max_retries, delegate);
+}
+
+void GaiaOAuthClient::Core::GetUserInfo(const std::string& oauth_access_token,
+ int max_retries,
+ Delegate* delegate) {
delegate_ = delegate;
num_retries_ = 0;
request_.reset(net::URLFetcher::Create(
@@ -145,6 +168,9 @@ void GaiaOAuthClient::Core::GetUserEmail(const std::string& oauth_access_token,
request_->SetRequestContext(request_context_getter_.get());
request_->AddExtraRequestHeader("Authorization: OAuth " + oauth_access_token);
request_->SetMaxRetriesOn5xx(max_retries);
+ request_->SetLoadFlags(net::LOAD_DO_NOT_SEND_COOKIES |
+ net::LOAD_DO_NOT_SAVE_COOKIES);
+
// Fetchers are sometimes cancelled because a network change was detected,
// especially at startup and after sign-in on ChromeOS. Retrying once should
// be enough in those cases; let the fetcher retry up to 3 times just in case.
@@ -180,6 +206,8 @@ void GaiaOAuthClient::Core::MakeGaiaRequest(
request_->SetRequestContext(request_context_getter_.get());
request_->SetUploadData("application/x-www-form-urlencoded", post_body);
request_->SetMaxRetriesOn5xx(max_retries);
+ request_->SetLoadFlags(net::LOAD_DO_NOT_SEND_COOKIES |
+ net::LOAD_DO_NOT_SAVE_COOKIES);
// See comment on SetAutomaticallyRetryOnNetworkChanges() above.
request_->SetAutomaticallyRetryOnNetworkChanges(3);
request_->Start();
@@ -250,13 +278,20 @@ void GaiaOAuthClient::Core::HandleResponse(
request_type_ = NO_PENDING_REQUEST;
switch (type) {
- case USER_INFO: {
+ case USER_EMAIL: {
std::string email;
response_dict->GetString("email", &email);
delegate_->OnGetUserEmailResponse(email);
break;
}
+ case USER_ID: {
+ std::string id;
+ response_dict->GetString("id", &id);
+ delegate_->OnGetUserIdResponse(id);
+ break;
+ }
+
case TOKEN_INFO: {
delegate_->OnGetTokenInfoResponse(response_dict.Pass());
break;
@@ -328,6 +363,12 @@ void GaiaOAuthClient::GetUserEmail(const std::string& access_token,
return core_->GetUserEmail(access_token, max_retries, delegate);
}
+void GaiaOAuthClient::GetUserId(const std::string& access_token,
+ int max_retries,
+ Delegate* delegate) {
+ return core_->GetUserId(access_token, max_retries, delegate);
+}
+
void GaiaOAuthClient::GetTokenInfo(const std::string& access_token,
int max_retries,
Delegate* delegate) {
diff --git a/chromium/google_apis/gaia/gaia_oauth_client.h b/chromium/google_apis/gaia/gaia_oauth_client.h
index 94d33137e84..14a26a63d7e 100644
--- a/chromium/google_apis/gaia/gaia_oauth_client.h
+++ b/chromium/google_apis/gaia/gaia_oauth_client.h
@@ -45,6 +45,8 @@ class GaiaOAuthClient {
int expires_in_seconds) {}
// Invoked on a successful response to the GetUserInfo request.
virtual void OnGetUserEmailResponse(const std::string& user_email) {}
+ // Invoked on a successful response to the GetUserId request.
+ virtual void OnGetUserIdResponse(const std::string& user_id) {}
// Invoked on a successful response to the GetTokenInfo request.
virtual void OnGetTokenInfoResponse(
scoped_ptr<DictionaryValue> token_info) {}
@@ -94,6 +96,14 @@ class GaiaOAuthClient {
int max_retries,
Delegate* delegate);
+ // Call the userinfo API, returning the user gaia ID associated
+ // with the given access token. The provided access token must have
+ // https://www.googleapis.com/auth/userinfo as one of its scopes.
+ // See |max_retries| docs above.
+ void GetUserId(const std::string& oauth_access_token,
+ int max_retries,
+ Delegate* delegate);
+
// Call the tokeninfo API, returning a dictionary of response values. The
// provided access token may have any scope, and basic results will be
// returned: issued_to, audience, scope, expires_in, access_type. In
diff --git a/chromium/google_apis/gaia/gaia_oauth_client_unittest.cc b/chromium/google_apis/gaia/gaia_oauth_client_unittest.cc
index cdeb4831154..32e7c6620b4 100644
--- a/chromium/google_apis/gaia/gaia_oauth_client_unittest.cc
+++ b/chromium/google_apis/gaia/gaia_oauth_client_unittest.cc
@@ -130,6 +130,7 @@ const std::string kTestAccessToken = "1/fFAGRNJru1FTz70BzhT3Zg";
const std::string kTestRefreshToken =
"1/6BMfW9j53gdGImsixUH6kU5RsR4zwI9lUVX-tqf8JXQ";
const std::string kTestUserEmail = "a_user@gmail.com";
+const std::string kTestUserId = "8675309";
const int kTestExpiresIn = 3920;
const std::string kDummyGetTokensResult =
@@ -144,6 +145,9 @@ const std::string kDummyRefreshTokenResult =
const std::string kDummyUserInfoResult =
"{\"email\":\"" + kTestUserEmail + "\"}";
+const std::string kDummyUserIdResult =
+ "{\"id\":\"" + kTestUserId + "\"}";
+
const std::string kDummyTokenInfoResult =
"{\"issued_to\": \"1234567890.apps.googleusercontent.com\","
"\"audience\": \"1234567890.apps.googleusercontent.com\","
@@ -186,6 +190,7 @@ class MockGaiaOAuthClientDelegate : public gaia::GaiaOAuthClient::Delegate {
MOCK_METHOD2(OnRefreshTokenResponse, void(const std::string& access_token,
int expires_in_seconds));
MOCK_METHOD1(OnGetUserEmailResponse, void(const std::string& user_email));
+ MOCK_METHOD1(OnGetUserIdResponse, void(const std::string& user_id));
MOCK_METHOD0(OnOAuthError, void());
MOCK_METHOD1(OnNetworkError, void(int response_code));
@@ -311,6 +316,17 @@ TEST_F(GaiaOAuthClientTest, GetUserEmail) {
auth.GetUserEmail("access_token", 1, &delegate);
}
+TEST_F(GaiaOAuthClientTest, GetUserId) {
+ MockGaiaOAuthClientDelegate delegate;
+ EXPECT_CALL(delegate, OnGetUserIdResponse(kTestUserId)).Times(1);
+
+ MockOAuthFetcherFactory factory;
+ factory.set_results(kDummyUserIdResult);
+
+ GaiaOAuthClient auth(GetRequestContext());
+ auth.GetUserId("access_token", 1, &delegate);
+}
+
TEST_F(GaiaOAuthClientTest, GetTokenInfo) {
const DictionaryValue* captured_result;
diff --git a/chromium/google_apis/gaia/gaia_urls.cc b/chromium/google_apis/gaia/gaia_urls.cc
index e5e5b8f32d0..053f7a37e29 100644
--- a/chromium/google_apis/gaia/gaia_urls.cc
+++ b/chromium/google_apis/gaia/gaia_urls.cc
@@ -28,6 +28,9 @@ const char kOAuthGetAccessTokenUrlSuffix[] = "OAuthGetAccessToken";
const char kOAuthWrapBridgeUrlSuffix[] = "OAuthWrapBridge";
const char kOAuth1LoginUrlSuffix[] = "OAuthLogin";
const char kOAuthRevokeTokenUrlSuffix[] = "AuthSubRevokeToken";
+const char kListAccountsSuffix[] = "ListAccounts";
+const char kEmbeddedSigninSuffix[] = "EmbeddedSignIn";
+const char kAddAccountSuffix[] = "AddSession";
// OAuth scopes
const char kOAuth1LoginScope[] = "https://www.google.com/accounts/OAuthLogin";
@@ -40,7 +43,6 @@ const char kClientLoginToOAuth2UrlSuffix[] = "o/oauth2/programmatic_auth";
const char kOAuth2AuthUrlSuffix[] = "o/oauth2/auth";
const char kOAuth2RevokeUrlSuffix[] = "o/oauth2/revoke";
const char kOAuth2TokenUrlSuffix[] = "o/oauth2/token";
-const char kClientOAuthUrlSuffix[] = "ClientOAuth";
// API calls from www.googleapis.com
const char kOAuth2IssueTokenUrlSuffix[] = "oauth2/v2/IssueToken";
@@ -104,6 +106,9 @@ GaiaUrls::GaiaUrls() {
oauth_wrap_bridge_url_ = gaia_url_.Resolve(kOAuthWrapBridgeUrlSuffix);
oauth_revoke_token_url_ = gaia_url_.Resolve(kOAuthRevokeTokenUrlSuffix);
oauth1_login_url_ = gaia_url_.Resolve(kOAuth1LoginUrlSuffix);
+ list_accounts_url_ = gaia_url_.Resolve(kListAccountsSuffix);
+ embedded_signin_url_ = gaia_url_.Resolve(kEmbeddedSigninSuffix);
+ add_account_url_ = gaia_url_.Resolve(kAddAccountSuffix);
// URLs from accounts.google.com (LSO).
get_oauth_token_url_ = lso_origin_url_.Resolve(kGetOAuthTokenUrlSuffix);
@@ -199,6 +204,18 @@ const GURL& GaiaUrls::oauth1_login_url() const {
return oauth1_login_url_;
}
+const GURL& GaiaUrls::list_accounts_url() const {
+ return list_accounts_url_;
+}
+
+const GURL& GaiaUrls::embedded_signin_url() const {
+ return embedded_signin_url_;
+}
+
+const GURL& GaiaUrls::add_account_url() const {
+ return add_account_url_;
+}
+
const std::string& GaiaUrls::oauth1_login_scope() const {
return oauth1_login_scope_;
}
diff --git a/chromium/google_apis/gaia/gaia_urls.h b/chromium/google_apis/gaia/gaia_urls.h
index e06b95d11e1..2ec6499663e 100644
--- a/chromium/google_apis/gaia/gaia_urls.h
+++ b/chromium/google_apis/gaia/gaia_urls.h
@@ -32,6 +32,9 @@ class GaiaUrls {
const GURL& oauth_user_info_url() const;
const GURL& oauth_revoke_token_url() const;
const GURL& oauth1_login_url() const;
+ const GURL& list_accounts_url() const;
+ const GURL& embedded_signin_url() const;
+ const GURL& add_account_url() const;
const std::string& oauth1_login_scope() const;
const std::string& oauth_wrap_bridge_user_info_scope() const;
@@ -73,6 +76,9 @@ class GaiaUrls {
GURL oauth_user_info_url_;
GURL oauth_revoke_token_url_;
GURL oauth1_login_url_;
+ GURL list_accounts_url_;
+ GURL embedded_signin_url_;
+ GURL add_account_url_;
std::string oauth1_login_scope_;
std::string oauth_wrap_bridge_user_info_scope_;
diff --git a/chromium/google_apis/gaia/google_service_auth_error.cc b/chromium/google_apis/gaia/google_service_auth_error.cc
index ab3d9c07de5..736bbd62785 100644
--- a/chromium/google_apis/gaia/google_service_auth_error.cc
+++ b/chromium/google_apis/gaia/google_service_auth_error.cc
@@ -147,7 +147,7 @@ const std::string& GoogleServiceAuthError::token() const {
default:
NOTREACHED();
}
- return EmptyString();
+ return base::EmptyString();
}
const std::string& GoogleServiceAuthError::error_message() const {
diff --git a/chromium/google_apis/gaia/oauth2_access_token_fetcher.cc b/chromium/google_apis/gaia/oauth2_access_token_fetcher.cc
index 44f2d4a7795..ab382056d4b 100644
--- a/chromium/google_apis/gaia/oauth2_access_token_fetcher.cc
+++ b/chromium/google_apis/gaia/oauth2_access_token_fetcher.cc
@@ -9,6 +9,8 @@
#include <vector>
#include "base/json/json_reader.h"
+#include "base/metrics/histogram.h"
+#include "base/metrics/sparse_histogram.h"
#include "base/strings/string_util.h"
#include "base/strings/stringprintf.h"
#include "base/time/time.h"
@@ -44,6 +46,38 @@ static const char kGetAccessTokenBodyWithScopeFormat[] =
static const char kAccessTokenKey[] = "access_token";
static const char kExpiresInKey[] = "expires_in";
+static const char kErrorKey[] = "error";
+
+// Enumerated constants for logging server responses on 400 errors, matching
+// RFC 6749.
+enum OAuth2ErrorCodesForHistogram {
+ OAUTH2_ACCESS_ERROR_INVALID_REQUEST = 0,
+ OAUTH2_ACCESS_ERROR_INVALID_CLIENT,
+ OAUTH2_ACCESS_ERROR_INVALID_GRANT,
+ OAUTH2_ACCESS_ERROR_UNAUTHORIZED_CLIENT,
+ OAUTH2_ACCESS_ERROR_UNSUPPORTED_GRANT_TYPE,
+ OAUTH2_ACCESS_ERROR_INVALID_SCOPE,
+ OAUTH2_ACCESS_ERROR_UNKNOWN,
+ OAUTH2_ACCESS_ERROR_COUNT
+};
+
+OAuth2ErrorCodesForHistogram OAuth2ErrorToHistogramValue(
+ const std::string& error) {
+ if (error == "invalid_request")
+ return OAUTH2_ACCESS_ERROR_INVALID_REQUEST;
+ else if (error == "invalid_client")
+ return OAUTH2_ACCESS_ERROR_INVALID_CLIENT;
+ else if (error == "invalid_grant")
+ return OAUTH2_ACCESS_ERROR_INVALID_GRANT;
+ else if (error == "unauthorized_client")
+ return OAUTH2_ACCESS_ERROR_UNAUTHORIZED_CLIENT;
+ else if (error == "unsupported_grant_type")
+ return OAUTH2_ACCESS_ERROR_UNSUPPORTED_GRANT_TYPE;
+ else if (error == "invalid_scope")
+ return OAUTH2_ACCESS_ERROR_INVALID_SCOPE;
+
+ return OAUTH2_ACCESS_ERROR_UNKNOWN;
+}
static GoogleServiceAuthError CreateAuthError(URLRequestStatus status) {
CHECK(!status.is_success());
@@ -124,31 +158,60 @@ void OAuth2AccessTokenFetcher::EndGetAccessToken(
state_ = GET_ACCESS_TOKEN_DONE;
URLRequestStatus status = source->GetStatus();
+ int histogram_value = status.is_success() ? source->GetResponseCode() :
+ status.error();
+ UMA_HISTOGRAM_SPARSE_SLOWLY("Gaia.ResponseCodesForOAuth2AccessToken",
+ histogram_value);
if (!status.is_success()) {
OnGetTokenFailure(CreateAuthError(status));
return;
}
- // HTTP_FORBIDDEN (403) is treated as temporary error, because it may be
- // '403 Rate Limit Exeeded.'
- if (source->GetResponseCode() == net::HTTP_FORBIDDEN) {
- OnGetTokenFailure(GoogleServiceAuthError(
- GoogleServiceAuthError::SERVICE_UNAVAILABLE));
- return;
- }
+ switch (source->GetResponseCode()) {
+ case net::HTTP_OK:
+ break;
+ case net::HTTP_FORBIDDEN:
+ case net::HTTP_INTERNAL_SERVER_ERROR:
+ // HTTP_FORBIDDEN (403) is treated as temporary error, because it may be
+ // '403 Rate Limit Exeeded.' 500 is always treated as transient.
+ OnGetTokenFailure(GoogleServiceAuthError(
+ GoogleServiceAuthError::SERVICE_UNAVAILABLE));
+ return;
+ case net::HTTP_BAD_REQUEST: {
+ // HTTP_BAD_REQUEST (400) usually contains error as per
+ // http://tools.ietf.org/html/rfc6749#section-5.2.
+ std::string gaia_error;
+ if (!ParseGetAccessTokenFailureResponse(source, &gaia_error)) {
+ OnGetTokenFailure(GoogleServiceAuthError(
+ GoogleServiceAuthError::SERVICE_ERROR));
+ return;
+ }
- // The other errors are treated as permanent error.
- if (source->GetResponseCode() != net::HTTP_OK) {
- OnGetTokenFailure(GoogleServiceAuthError(
- GoogleServiceAuthError::INVALID_GAIA_CREDENTIALS));
- return;
+ OAuth2ErrorCodesForHistogram access_error(OAuth2ErrorToHistogramValue(
+ gaia_error));
+ UMA_HISTOGRAM_ENUMERATION("Gaia.BadRequestTypeForOAuth2AccessToken",
+ access_error, OAUTH2_ACCESS_ERROR_COUNT);
+
+ OnGetTokenFailure(access_error == OAUTH2_ACCESS_ERROR_INVALID_GRANT ?
+ GoogleServiceAuthError(
+ GoogleServiceAuthError::INVALID_GAIA_CREDENTIALS) :
+ GoogleServiceAuthError(
+ GoogleServiceAuthError::SERVICE_ERROR));
+ return;
+ }
+ default:
+ // The other errors are treated as permanent error.
+ OnGetTokenFailure(GoogleServiceAuthError(
+ GoogleServiceAuthError::INVALID_GAIA_CREDENTIALS));
+ return;
}
// The request was successfully fetched and it returned OK.
// Parse out the access token and the expiration time.
std::string access_token;
int expires_in;
- if (!ParseGetAccessTokenResponse(source, &access_token, &expires_in)) {
+ if (!ParseGetAccessTokenSuccessResponse(
+ source, &access_token, &expires_in)) {
DLOG(WARNING) << "Response doesn't match expected format";
OnGetTokenFailure(
GoogleServiceAuthError(GoogleServiceAuthError::SERVICE_UNAVAILABLE));
@@ -213,21 +276,43 @@ std::string OAuth2AccessTokenFetcher::MakeGetAccessTokenBody(
}
}
-// static
-bool OAuth2AccessTokenFetcher::ParseGetAccessTokenResponse(
- const net::URLFetcher* source,
- std::string* access_token,
- int* expires_in) {
+scoped_ptr<base::DictionaryValue> ParseGetAccessTokenResponse(
+ const net::URLFetcher* source) {
CHECK(source);
- CHECK(access_token);
+
std::string data;
source->GetResponseAsString(&data);
scoped_ptr<base::Value> value(base::JSONReader::Read(data));
if (!value.get() || value->GetType() != base::Value::TYPE_DICTIONARY)
+ value.reset();
+
+ return scoped_ptr<base::DictionaryValue>(
+ static_cast<base::DictionaryValue*>(value.release()));
+}
+
+// static
+bool OAuth2AccessTokenFetcher::ParseGetAccessTokenSuccessResponse(
+ const net::URLFetcher* source,
+ std::string* access_token,
+ int* expires_in) {
+ CHECK(access_token);
+ scoped_ptr<base::DictionaryValue> value = ParseGetAccessTokenResponse(
+ source);
+ if (value.get() == NULL)
return false;
- base::DictionaryValue* dict =
- static_cast<base::DictionaryValue*>(value.get());
- return dict->GetString(kAccessTokenKey, access_token) &&
- dict->GetInteger(kExpiresInKey, expires_in);
+ return value->GetString(kAccessTokenKey, access_token) &&
+ value->GetInteger(kExpiresInKey, expires_in);
+}
+
+// static
+bool OAuth2AccessTokenFetcher::ParseGetAccessTokenFailureResponse(
+ const net::URLFetcher* source,
+ std::string* error) {
+ CHECK(error);
+ scoped_ptr<base::DictionaryValue> value = ParseGetAccessTokenResponse(
+ source);
+ if (value.get() == NULL)
+ return false;
+ return value->GetString(kErrorKey, error);
}
diff --git a/chromium/google_apis/gaia/oauth2_access_token_fetcher.h b/chromium/google_apis/gaia/oauth2_access_token_fetcher.h
index 11ac6ea8b5c..90805c09963 100644
--- a/chromium/google_apis/gaia/oauth2_access_token_fetcher.h
+++ b/chromium/google_apis/gaia/oauth2_access_token_fetcher.h
@@ -90,9 +90,15 @@ class OAuth2AccessTokenFetcher : public net::URLFetcherDelegate {
const std::string& client_secret,
const std::string& refresh_token,
const std::vector<std::string>& scopes);
- static bool ParseGetAccessTokenResponse(const net::URLFetcher* source,
- std::string* access_token,
- int* expires_in);
+
+ static bool ParseGetAccessTokenSuccessResponse(
+ const net::URLFetcher* source,
+ std::string* access_token,
+ int* expires_in);
+
+ static bool ParseGetAccessTokenFailureResponse(
+ const net::URLFetcher* source,
+ std::string* error);
// State that is set during construction.
OAuth2AccessTokenConsumer* const consumer_;
diff --git a/chromium/google_apis/gaia/oauth2_access_token_fetcher_unittest.cc b/chromium/google_apis/gaia/oauth2_access_token_fetcher_unittest.cc
index 6b12da3fbd0..135e292d7c0 100644
--- a/chromium/google_apis/gaia/oauth2_access_token_fetcher_unittest.cc
+++ b/chromium/google_apis/gaia/oauth2_access_token_fetcher_unittest.cc
@@ -50,6 +50,11 @@ static const char kTokenResponseNoAccessToken[] =
" \"token_type\": \"Bearer\""
"}";
+static const char kValidFailureTokenResponse[] =
+ "{"
+ " \"error\": \"invalid_grant\""
+ "}";
+
class MockUrlFetcherFactory : public ScopedURLFetcherFactory,
public URLFetcherFactory {
public:
@@ -195,7 +200,7 @@ TEST_F(OAuth2AccessTokenFetcherTest, MAYBE_ParseGetAccessTokenResponse) {
std::string at;
int expires_in;
- EXPECT_FALSE(OAuth2AccessTokenFetcher::ParseGetAccessTokenResponse(
+ EXPECT_FALSE(OAuth2AccessTokenFetcher::ParseGetAccessTokenSuccessResponse(
&url_fetcher, &at, &expires_in));
EXPECT_TRUE(at.empty());
}
@@ -205,7 +210,7 @@ TEST_F(OAuth2AccessTokenFetcherTest, MAYBE_ParseGetAccessTokenResponse) {
std::string at;
int expires_in;
- EXPECT_FALSE(OAuth2AccessTokenFetcher::ParseGetAccessTokenResponse(
+ EXPECT_FALSE(OAuth2AccessTokenFetcher::ParseGetAccessTokenSuccessResponse(
&url_fetcher, &at, &expires_in));
EXPECT_TRUE(at.empty());
}
@@ -215,7 +220,7 @@ TEST_F(OAuth2AccessTokenFetcherTest, MAYBE_ParseGetAccessTokenResponse) {
std::string at;
int expires_in;
- EXPECT_FALSE(OAuth2AccessTokenFetcher::ParseGetAccessTokenResponse(
+ EXPECT_FALSE(OAuth2AccessTokenFetcher::ParseGetAccessTokenSuccessResponse(
&url_fetcher, &at, &expires_in));
EXPECT_TRUE(at.empty());
}
@@ -225,9 +230,27 @@ TEST_F(OAuth2AccessTokenFetcherTest, MAYBE_ParseGetAccessTokenResponse) {
std::string at;
int expires_in;
- EXPECT_TRUE(OAuth2AccessTokenFetcher::ParseGetAccessTokenResponse(
+ EXPECT_TRUE(OAuth2AccessTokenFetcher::ParseGetAccessTokenSuccessResponse(
&url_fetcher, &at, &expires_in));
EXPECT_EQ("at1", at);
EXPECT_EQ(3600, expires_in);
}
+ { // Valid json: invalid error response.
+ TestURLFetcher url_fetcher(0, GURL("www.google.com"), NULL);
+ url_fetcher.SetResponseString(kTokenResponseNoAccessToken);
+
+ std::string error;
+ EXPECT_FALSE(OAuth2AccessTokenFetcher::ParseGetAccessTokenFailureResponse(
+ &url_fetcher, &error));
+ EXPECT_TRUE(error.empty());
+ }
+ { // Valid json: error response.
+ TestURLFetcher url_fetcher(0, GURL("www.google.com"), NULL);
+ url_fetcher.SetResponseString(kValidFailureTokenResponse);
+
+ std::string error;
+ EXPECT_TRUE(OAuth2AccessTokenFetcher::ParseGetAccessTokenFailureResponse(
+ &url_fetcher, &error));
+ EXPECT_EQ("invalid_grant", error);
+ }
}
diff --git a/chromium/google_apis/gaia/oauth2_mint_token_flow.cc b/chromium/google_apis/gaia/oauth2_mint_token_flow.cc
index b705ab872b0..a1e3ff3cfeb 100644
--- a/chromium/google_apis/gaia/oauth2_mint_token_flow.cc
+++ b/chromium/google_apis/gaia/oauth2_mint_token_flow.cc
@@ -30,29 +30,28 @@ using net::URLRequestStatus;
namespace {
-static const char kForceValueFalse[] = "false";
-static const char kForceValueTrue[] = "true";
-static const char kResponseTypeValueNone[] = "none";
-static const char kResponseTypeValueToken[] = "token";
+const char kForceValueFalse[] = "false";
+const char kForceValueTrue[] = "true";
+const char kResponseTypeValueNone[] = "none";
+const char kResponseTypeValueToken[] = "token";
-static const char kOAuth2IssueTokenBodyFormat[] =
+const char kOAuth2IssueTokenBodyFormat[] =
"force=%s"
"&response_type=%s"
"&scope=%s"
"&client_id=%s"
"&origin=%s";
-static const char kIssueAdviceKey[] = "issueAdvice";
-static const char kIssueAdviceValueAuto[] = "auto";
-static const char kIssueAdviceValueConsent[] = "consent";
-static const char kAccessTokenKey[] = "token";
-static const char kConsentKey[] = "consent";
-static const char kExpiresInKey[] = "expiresIn";
-static const char kScopesKey[] = "scopes";
-static const char kDescriptionKey[] = "description";
-static const char kDetailKey[] = "detail";
-static const char kDetailSeparators[] = "\n";
-static const char kError[] = "error";
-static const char kMessage[] = "message";
+const char kIssueAdviceKey[] = "issueAdvice";
+const char kIssueAdviceValueConsent[] = "consent";
+const char kAccessTokenKey[] = "token";
+const char kConsentKey[] = "consent";
+const char kExpiresInKey[] = "expiresIn";
+const char kScopesKey[] = "scopes";
+const char kDescriptionKey[] = "description";
+const char kDetailKey[] = "detail";
+const char kDetailSeparators[] = "\n";
+const char kError[] = "error";
+const char kMessage[] = "message";
static GoogleServiceAuthError CreateAuthError(const net::URLFetcher* source) {
URLRequestStatus status = source->GetStatus();
@@ -259,7 +258,7 @@ bool OAuth2MintTokenFlow::ParseIssueAdviceResponse(
for (size_t index = 0; index < scopes_list->GetSize(); ++index) {
const base::DictionaryValue* scopes_entry = NULL;
IssueAdviceInfoEntry entry;
- string16 detail;
+ base::string16 detail;
if (!scopes_list->GetDictionary(index, &scopes_entry) ||
!scopes_entry->GetString(kDescriptionKey, &entry.description) ||
!scopes_entry->GetString(kDetailKey, &detail)) {
@@ -268,7 +267,8 @@ bool OAuth2MintTokenFlow::ParseIssueAdviceResponse(
}
TrimWhitespace(entry.description, TRIM_ALL, &entry.description);
- static const string16 detail_separators = ASCIIToUTF16(kDetailSeparators);
+ static const base::string16 detail_separators =
+ ASCIIToUTF16(kDetailSeparators);
Tokenize(detail, detail_separators, &entry.details);
for (size_t i = 0; i < entry.details.size(); i++)
TrimWhitespace(entry.details[i], TRIM_ALL, &entry.details[i]);
diff --git a/chromium/google_apis/gaia/oauth2_mint_token_flow.h b/chromium/google_apis/gaia/oauth2_mint_token_flow.h
index 5bb751d6c71..06c823ce4bc 100644
--- a/chromium/google_apis/gaia/oauth2_mint_token_flow.h
+++ b/chromium/google_apis/gaia/oauth2_mint_token_flow.h
@@ -43,8 +43,8 @@ struct IssueAdviceInfoEntry {
IssueAdviceInfoEntry();
~IssueAdviceInfoEntry();
- string16 description;
- std::vector<string16> details;
+ base::string16 description;
+ std::vector<base::string16> details;
bool operator==(const IssueAdviceInfoEntry& rhs) const;
};
diff --git a/chromium/google_apis/gaia/oauth2_token_service.cc b/chromium/google_apis/gaia/oauth2_token_service.cc
index 3259e283b64..9684281097b 100644
--- a/chromium/google_apis/gaia/oauth2_token_service.cc
+++ b/chromium/google_apis/gaia/oauth2_token_service.cc
@@ -47,14 +47,20 @@ bool OAuth2TokenService::RequestParameters::operator<(
}
OAuth2TokenService::RequestImpl::RequestImpl(
+ const std::string& account_id,
OAuth2TokenService::Consumer* consumer)
- : consumer_(consumer) {
+ : account_id_(account_id),
+ consumer_(consumer) {
}
OAuth2TokenService::RequestImpl::~RequestImpl() {
DCHECK(CalledOnValidThread());
}
+std::string OAuth2TokenService::RequestImpl::GetAccountId() const {
+ return account_id_;
+}
+
void OAuth2TokenService::RequestImpl::InformConsumer(
const GoogleServiceAuthError& error,
const std::string& access_token,
@@ -445,7 +451,7 @@ OAuth2TokenService::StartRequestForClientWithContext(
Consumer* consumer) {
DCHECK(CalledOnValidThread());
- scoped_ptr<RequestImpl> request(new RequestImpl(consumer));
+ scoped_ptr<RequestImpl> request = CreateRequest(account_id, consumer);
if (!RefreshTokenIsAvailable(account_id)) {
base::MessageLoop::current()->PostTask(FROM_HERE, base::Bind(
@@ -473,6 +479,12 @@ OAuth2TokenService::StartRequestForClientWithContext(
return request.PassAs<Request>();
}
+scoped_ptr<OAuth2TokenService::RequestImpl> OAuth2TokenService::CreateRequest(
+ const std::string& account_id,
+ Consumer* consumer) {
+ return scoped_ptr<RequestImpl>(new RequestImpl(account_id, consumer));
+}
+
void OAuth2TokenService::FetchOAuth2Token(RequestImpl* request,
const std::string& account_id,
net::URLRequestContextGetter* getter,
diff --git a/chromium/google_apis/gaia/oauth2_token_service.h b/chromium/google_apis/gaia/oauth2_token_service.h
index d092430f9a5..1745315abf4 100644
--- a/chromium/google_apis/gaia/oauth2_token_service.h
+++ b/chromium/google_apis/gaia/oauth2_token_service.h
@@ -56,6 +56,7 @@ class OAuth2TokenService : public base::NonThreadSafe {
class Request {
public:
virtual ~Request();
+ virtual std::string GetAccountId() const = 0;
protected:
Request();
};
@@ -111,11 +112,9 @@ class OAuth2TokenService : public base::NonThreadSafe {
// |scopes| is the set of scopes to get an access token for, |consumer| is
// the object that will be called back with results if the returned request
// is not deleted.
- // TODO(atwilson): Make this non-virtual when we change
- // ProfileOAuth2TokenServiceRequestTest to use FakeProfileOAuth2TokenService.
- virtual scoped_ptr<Request> StartRequest(const std::string& account_id,
- const ScopeSet& scopes,
- Consumer* consumer);
+ scoped_ptr<Request> StartRequest(const std::string& account_id,
+ const ScopeSet& scopes,
+ Consumer* consumer);
// This method does the same as |StartRequest| except it uses |client_id| and
// |client_secret| to identify OAuth client app instead of using
@@ -179,9 +178,12 @@ class OAuth2TokenService : public base::NonThreadSafe {
public Request {
public:
// |consumer| is required to outlive this.
- explicit RequestImpl(Consumer* consumer);
+ explicit RequestImpl(const std::string& account_id, Consumer* consumer);
virtual ~RequestImpl();
+ // Overridden from Request:
+ virtual std::string GetAccountId() const OVERRIDE;
+
// Informs |consumer_| that this request is completed.
void InformConsumer(const GoogleServiceAuthError& error,
const std::string& access_token,
@@ -189,6 +191,7 @@ class OAuth2TokenService : public base::NonThreadSafe {
private:
// |consumer_| to call back when this request completes.
+ const std::string account_id_;
Consumer* const consumer_;
};
@@ -225,9 +228,16 @@ class OAuth2TokenService : public base::NonThreadSafe {
void CancelRequestsForAccount(const std::string& account_id);
// Called by subclasses to notify observers.
- void FireRefreshTokenAvailable(const std::string& account_id);
- void FireRefreshTokenRevoked(const std::string& account_id);
- void FireRefreshTokensLoaded();
+ virtual void FireRefreshTokenAvailable(const std::string& account_id);
+ virtual void FireRefreshTokenRevoked(const std::string& account_id);
+ virtual void FireRefreshTokensLoaded();
+
+ // Creates a request implementation. Can be overriden by derived classes to
+ // provide additional control of token consumption. |consumer| will outlive
+ // the created request.
+ virtual scoped_ptr<RequestImpl> CreateRequest(
+ const std::string& account_id,
+ Consumer* consumer);
// Fetches an OAuth token for the specified client/scopes. Virtual so it can
// be overridden for tests and for platform-specific behavior on Android.
diff --git a/chromium/google_apis/gaia/oauth_request_signer.cc b/chromium/google_apis/gaia/oauth_request_signer.cc
index b16744cbccd..115c14d6f01 100644
--- a/chromium/google_apis/gaia/oauth_request_signer.cc
+++ b/chromium/google_apis/gaia/oauth_request_signer.cc
@@ -24,26 +24,24 @@
namespace {
-static const int kHexBase = 16;
-static char kHexDigits[] = "0123456789ABCDEF";
-static const size_t kHmacDigestLength = 20;
-static const int kMaxNonceLength = 30;
-static const int kMinNonceLength = 15;
+const int kHexBase = 16;
+char kHexDigits[] = "0123456789ABCDEF";
+const size_t kHmacDigestLength = 20;
+const int kMaxNonceLength = 30;
+const int kMinNonceLength = 15;
-static const char kOAuthConsumerKeyLabel[] = "oauth_consumer_key";
-static const char kOAuthConsumerSecretLabel[] = "oauth_consumer_secret";
-static const char kOAuthNonceCharacters[] =
+const char kOAuthConsumerKeyLabel[] = "oauth_consumer_key";
+const char kOAuthNonceCharacters[] =
"abcdefghijklmnopqrstuvwyz"
"ABCDEFGHIJKLMNOPQRSTUVWYZ"
"0123456789_";
-static const char kOAuthNonceLabel[] = "oauth_nonce";
-static const char kOAuthSignatureLabel[] = "oauth_signature";
-static const char kOAuthSignatureMethodLabel[] = "oauth_signature_method";
-static const char kOAuthTimestampLabel[] = "oauth_timestamp";
-static const char kOAuthTokenLabel[] = "oauth_token";
-static const char kOAuthTokenSecretLabel[] = "oauth_token_secret";
-static const char kOAuthVersion[] = "1.0";
-static const char kOAuthVersionLabel[] = "oauth_version";
+const char kOAuthNonceLabel[] = "oauth_nonce";
+const char kOAuthSignatureLabel[] = "oauth_signature";
+const char kOAuthSignatureMethodLabel[] = "oauth_signature_method";
+const char kOAuthTimestampLabel[] = "oauth_timestamp";
+const char kOAuthTokenLabel[] = "oauth_token";
+const char kOAuthVersion[] = "1.0";
+const char kOAuthVersionLabel[] = "oauth_version";
enum ParseQueryState {
START_STATE,
@@ -206,10 +204,12 @@ bool SignHmacSha1(const std::string& text,
DCHECK(hmac.DigestLength() == kHmacDigestLength);
unsigned char digest[kHmacDigestLength];
bool result = hmac.Init(key) &&
- hmac.Sign(text, digest, kHmacDigestLength) &&
- base::Base64Encode(std::string(reinterpret_cast<const char*>(digest),
- kHmacDigestLength),
- signature_return);
+ hmac.Sign(text, digest, kHmacDigestLength);
+ if (result) {
+ base::Base64Encode(
+ std::string(reinterpret_cast<const char*>(digest), kHmacDigestLength),
+ signature_return);
+ }
return result;
}
diff --git a/chromium/google_apis/gcm/DEPS b/chromium/google_apis/gcm/DEPS
new file mode 100644
index 00000000000..08ac400e0d4
--- /dev/null
+++ b/chromium/google_apis/gcm/DEPS
@@ -0,0 +1,14 @@
+include_rules = [
+ # Repeat these from the top-level DEPS file so one can just run
+ #
+ # checkdeps.py google_apis/gcm
+ #
+ # to test.
+ "+base",
+ "+testing",
+
+ "+components/webdata/encryptor",
+ "+google", # For third_party/protobuf/src.
+ "+net",
+ "+third_party/leveldatabase",
+]
diff --git a/chromium/google_apis/gcm/OWNERS b/chromium/google_apis/gcm/OWNERS
new file mode 100644
index 00000000000..493c918a07c
--- /dev/null
+++ b/chromium/google_apis/gcm/OWNERS
@@ -0,0 +1,3 @@
+zea@chromium.org
+dimich@chromium.org
+tim@chromium.org
diff --git a/chromium/google_apis/gcm/base/gcm_export.h b/chromium/google_apis/gcm/base/gcm_export.h
new file mode 100644
index 00000000000..b66eb8e54cf
--- /dev/null
+++ b/chromium/google_apis/gcm/base/gcm_export.h
@@ -0,0 +1,29 @@
+// Copyright 2013 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef GOOGLE_APIS_GCM_GCM_EXPORT_H_
+#define GOOGLE_APIS_GCM_GCM_EXPORT_H_
+
+#if defined(COMPONENT_BUILD)
+#if defined(WIN32)
+
+#if defined(GCM_IMPLEMENTATION)
+#define GCM_EXPORT __declspec(dllexport)
+#else
+#define GCM_EXPORT __declspec(dllimport)
+#endif // defined(GCM_IMPLEMENTATION)
+
+#else // defined(WIN32)
+#if defined(GCM_IMPLEMENTATION)
+#define GCM_EXPORT __attribute__((visibility("default")))
+#else
+#define GCM_EXPORT
+#endif // defined(GCM_IMPLEMENTATION)
+#endif
+
+#else // defined(COMPONENT_BUILD)
+#define GCM_EXPORT
+#endif
+
+#endif // GOOGLE_APIS_GCM_GCM_EXPORT_H_
diff --git a/chromium/google_apis/gcm/base/mcs_message.cc b/chromium/google_apis/gcm/base/mcs_message.cc
new file mode 100644
index 00000000000..f0c130baf89
--- /dev/null
+++ b/chromium/google_apis/gcm/base/mcs_message.cc
@@ -0,0 +1,78 @@
+// Copyright 2013 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "google_apis/gcm/base/mcs_message.h"
+
+#include "base/logging.h"
+#include "google_apis/gcm/base/mcs_util.h"
+
+namespace gcm {
+
+MCSMessage::Core::Core() {}
+
+MCSMessage::Core::Core(uint8 tag,
+ const google::protobuf::MessageLite& protobuf) {
+ scoped_ptr<google::protobuf::MessageLite> owned_protobuf(protobuf.New());
+ owned_protobuf->CheckTypeAndMergeFrom(protobuf);
+ protobuf_ = owned_protobuf.Pass();
+}
+
+MCSMessage::Core::Core(
+ uint8 tag,
+ scoped_ptr<const google::protobuf::MessageLite> protobuf) {
+ protobuf_ = protobuf.Pass();
+}
+
+MCSMessage::Core::~Core() {}
+
+const google::protobuf::MessageLite& MCSMessage::Core::Get() const {
+ return *protobuf_;
+}
+
+MCSMessage::MCSMessage() : tag_(0), size_(0) {}
+
+MCSMessage::MCSMessage(const google::protobuf::MessageLite& protobuf)
+ : tag_(GetMCSProtoTag(protobuf)),
+ size_(protobuf.ByteSize()),
+ core_(new Core(tag_, protobuf)) {
+}
+
+MCSMessage::MCSMessage(uint8 tag,
+ const google::protobuf::MessageLite& protobuf)
+ : tag_(tag),
+ size_(protobuf.ByteSize()),
+ core_(new Core(tag_, protobuf)) {
+ DCHECK_EQ(tag, GetMCSProtoTag(protobuf));
+}
+
+MCSMessage::MCSMessage(uint8 tag,
+ scoped_ptr<const google::protobuf::MessageLite> protobuf)
+ : tag_(tag),
+ size_(protobuf->ByteSize()),
+ core_(new Core(tag_, protobuf.Pass())) {
+ DCHECK_EQ(tag, GetMCSProtoTag(core_->Get()));
+}
+
+MCSMessage::~MCSMessage() {
+}
+
+bool MCSMessage::IsValid() const {
+ return core_.get() != NULL;
+}
+
+std::string MCSMessage::SerializeAsString() const {
+ return core_->Get().SerializeAsString();
+}
+
+const google::protobuf::MessageLite& MCSMessage::GetProtobuf() const {
+ return core_->Get();
+}
+
+scoped_ptr<google::protobuf::MessageLite> MCSMessage::CloneProtobuf() const {
+ scoped_ptr<google::protobuf::MessageLite> clone(GetProtobuf().New());
+ clone->CheckTypeAndMergeFrom(GetProtobuf());
+ return clone.Pass();
+}
+
+} // namespace gcm
diff --git a/chromium/google_apis/gcm/base/mcs_message.h b/chromium/google_apis/gcm/base/mcs_message.h
new file mode 100644
index 00000000000..14d80ed3b97
--- /dev/null
+++ b/chromium/google_apis/gcm/base/mcs_message.h
@@ -0,0 +1,85 @@
+// Copyright 2013 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef GOOGLE_APIS_GCM_BASE_MCS_MESSAGE_H_
+#define GOOGLE_APIS_GCM_BASE_MCS_MESSAGE_H_
+
+#include <string>
+
+#include "base/basictypes.h"
+#include "base/memory/ref_counted.h"
+#include "base/memory/scoped_ptr.h"
+#include "google_apis/gcm/base/gcm_export.h"
+
+namespace google {
+namespace protobuf {
+class MessageLite;
+} // namespace protobuf
+} // namespace google
+
+namespace gcm {
+
+// A wrapper for MCS protobuffers that encapsulates their tag, size and data
+// in an immutable and thread-safe format. If a mutable version is desired,
+// CloneProtobuf() should use used to create a new copy of the protobuf.
+//
+// Note: default copy and assign welcome.
+class GCM_EXPORT MCSMessage {
+ public:
+ // Creates an invalid MCSMessage.
+ MCSMessage();
+ // Infers tag from |message|.
+ explicit MCSMessage(const google::protobuf::MessageLite& protobuf);
+ // |tag| must match |protobuf|'s message type.
+ MCSMessage(uint8 tag, const google::protobuf::MessageLite& protobuf);
+ // |tag| must match |protobuf|'s message type. Takes ownership of |protobuf|.
+ MCSMessage(uint8 tag,
+ scoped_ptr<const google::protobuf::MessageLite> protobuf);
+ ~MCSMessage();
+
+ // Returns whether this message is valid or not (whether a protobuf was
+ // provided at construction time or not).
+ bool IsValid() const;
+
+ // Getters for serialization.
+ uint8 tag() const { return tag_; }
+ int size() const {return size_; }
+ std::string SerializeAsString() const;
+
+ // Getter for accessing immutable probotuf fields.
+ const google::protobuf::MessageLite& GetProtobuf() const;
+
+ // Getter for creating a mutated version of the protobuf.
+ scoped_ptr<google::protobuf::MessageLite> CloneProtobuf() const;
+
+ private:
+ class Core : public base::RefCountedThreadSafe<MCSMessage::Core> {
+ public:
+ Core();
+ Core(uint8 tag, const google::protobuf::MessageLite& protobuf);
+ Core(uint8 tag, scoped_ptr<const google::protobuf::MessageLite> protobuf);
+
+ const google::protobuf::MessageLite& Get() const;
+
+ private:
+ friend class base::RefCountedThreadSafe<MCSMessage::Core>;
+ ~Core();
+
+ // The immutable protobuf.
+ scoped_ptr<const google::protobuf::MessageLite> protobuf_;
+
+ DISALLOW_COPY_AND_ASSIGN(Core);
+ };
+
+ // These are cached separately to avoid having to recompute them.
+ const uint8 tag_;
+ const int size_;
+
+ // The refcounted core, containing the protobuf memory.
+ scoped_refptr<const Core> core_;
+};
+
+} // namespace gcm
+
+#endif // GOOGLE_APIS_GCM_BASE_MCS_MESSAGE_H_
diff --git a/chromium/google_apis/gcm/base/mcs_message_unittest.cc b/chromium/google_apis/gcm/base/mcs_message_unittest.cc
new file mode 100644
index 00000000000..4d4ef598ad2
--- /dev/null
+++ b/chromium/google_apis/gcm/base/mcs_message_unittest.cc
@@ -0,0 +1,92 @@
+// Copyright 2013 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "google_apis/gcm/base/mcs_message.h"
+
+#include "base/logging.h"
+#include "base/message_loop/message_loop.h"
+#include "google_apis/gcm/base/mcs_util.h"
+#include "google_apis/gcm/protocol/mcs.pb.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+namespace gcm {
+
+const uint64 kAndroidId = 12345;
+const uint64 kSecret = 54321;
+
+class MCSMessageTest : public testing::Test {
+ public:
+ MCSMessageTest();
+ virtual ~MCSMessageTest();
+ private:
+ base::MessageLoop message_loop_;
+};
+
+MCSMessageTest::MCSMessageTest() {
+}
+
+MCSMessageTest::~MCSMessageTest() {
+}
+
+TEST_F(MCSMessageTest, Invalid) {
+ MCSMessage message;
+ EXPECT_FALSE(message.IsValid());
+}
+
+TEST_F(MCSMessageTest, InitInferTag) {
+ scoped_ptr<mcs_proto::LoginRequest> login_request(
+ BuildLoginRequest(kAndroidId, kSecret));
+ scoped_ptr<google::protobuf::MessageLite> login_copy(
+ new mcs_proto::LoginRequest(*login_request));
+ MCSMessage message(*login_copy);
+ login_copy.reset();
+ ASSERT_TRUE(message.IsValid());
+ EXPECT_EQ(kLoginRequestTag, message.tag());
+ EXPECT_EQ(login_request->ByteSize(), message.size());
+ EXPECT_EQ(login_request->SerializeAsString(), message.SerializeAsString());
+ EXPECT_EQ(login_request->SerializeAsString(),
+ message.GetProtobuf().SerializeAsString());
+ login_copy = message.CloneProtobuf();
+ EXPECT_EQ(login_request->SerializeAsString(),
+ login_copy->SerializeAsString());
+}
+
+TEST_F(MCSMessageTest, InitWithTag) {
+ scoped_ptr<mcs_proto::LoginRequest> login_request(
+ BuildLoginRequest(kAndroidId, kSecret));
+ scoped_ptr<google::protobuf::MessageLite> login_copy(
+ new mcs_proto::LoginRequest(*login_request));
+ MCSMessage message(kLoginRequestTag, *login_copy);
+ login_copy.reset();
+ ASSERT_TRUE(message.IsValid());
+ EXPECT_EQ(kLoginRequestTag, message.tag());
+ EXPECT_EQ(login_request->ByteSize(), message.size());
+ EXPECT_EQ(login_request->SerializeAsString(), message.SerializeAsString());
+ EXPECT_EQ(login_request->SerializeAsString(),
+ message.GetProtobuf().SerializeAsString());
+ login_copy = message.CloneProtobuf();
+ EXPECT_EQ(login_request->SerializeAsString(),
+ login_copy->SerializeAsString());
+}
+
+TEST_F(MCSMessageTest, InitPassOwnership) {
+ scoped_ptr<mcs_proto::LoginRequest> login_request(
+ BuildLoginRequest(kAndroidId, kSecret));
+ scoped_ptr<google::protobuf::MessageLite> login_copy(
+ new mcs_proto::LoginRequest(*login_request));
+ MCSMessage message(kLoginRequestTag,
+ login_copy.PassAs<const google::protobuf::MessageLite>());
+ EXPECT_FALSE(login_copy.get());
+ ASSERT_TRUE(message.IsValid());
+ EXPECT_EQ(kLoginRequestTag, message.tag());
+ EXPECT_EQ(login_request->ByteSize(), message.size());
+ EXPECT_EQ(login_request->SerializeAsString(), message.SerializeAsString());
+ EXPECT_EQ(login_request->SerializeAsString(),
+ message.GetProtobuf().SerializeAsString());
+ login_copy = message.CloneProtobuf();
+ EXPECT_EQ(login_request->SerializeAsString(),
+ login_copy->SerializeAsString());
+}
+
+} // namespace gcm
diff --git a/chromium/google_apis/gcm/base/mcs_util.cc b/chromium/google_apis/gcm/base/mcs_util.cc
new file mode 100644
index 00000000000..736556079f8
--- /dev/null
+++ b/chromium/google_apis/gcm/base/mcs_util.cc
@@ -0,0 +1,233 @@
+// Copyright 2013 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "google_apis/gcm/base/mcs_util.h"
+
+#include "base/format_macros.h"
+#include "base/logging.h"
+#include "base/strings/string_number_conversions.h"
+#include "base/strings/stringprintf.h"
+
+namespace gcm {
+
+namespace {
+
+// Type names corresponding to MCSProtoTags. Useful for identifying what type
+// of MCS protobuf is contained within a google::protobuf::MessageLite object.
+// WARNING: must match the order in MCSProtoTag.
+const char* kProtoNames[] = {
+ "mcs_proto.HeartbeatPing",
+ "mcs_proto.HeartbeatAck",
+ "mcs_proto.LoginRequest",
+ "mcs_proto.LoginResponse",
+ "mcs_proto.Close",
+ "mcs_proto.MessageStanza",
+ "mcs_proto.PresenceStanza",
+ "mcs_proto.IqStanza",
+ "mcs_proto.DataMessageStanza",
+ "mcs_proto.BatchPresenceStanza",
+ "mcs_proto.StreamErrorStanza",
+ "mcs_proto.HttpRequest",
+ "mcs_proto.HttpResponse",
+ "mcs_proto.BindAccountRequest",
+ "mcs_proto.BindAccountResponse",
+ "mcs_proto.TalkMetadata"
+};
+COMPILE_ASSERT(arraysize(kProtoNames) == kNumProtoTypes,
+ ProtoNamesMustIncludeAllTags);
+
+// TODO(zea): replace these with proper values.
+const char kLoginId[] = "login-1";
+const char kLoginDomain[] = "mcs.android.com";
+const char kLoginDeviceIdPrefix[] = "android-";
+const char kLoginSettingName[] = "new_vc";
+const char kLoginSettingValue[] = "1";
+
+} // namespace
+
+scoped_ptr<mcs_proto::LoginRequest> BuildLoginRequest(uint64 auth_id,
+ uint64 auth_token) {
+ // Create a hex encoded auth id for the device id field.
+ std::string auth_id_hex;
+ auth_id_hex = base::StringPrintf("%" PRIx64, auth_id);
+
+ std::string auth_id_str = base::Uint64ToString(auth_id);
+ std::string auth_token_str = base::Uint64ToString(auth_token);
+
+ scoped_ptr<mcs_proto::LoginRequest> login_request(
+ new mcs_proto::LoginRequest());
+
+ // TODO(zea): set better values.
+ login_request->set_account_id(1000000);
+ login_request->set_adaptive_heartbeat(false);
+ login_request->set_auth_service(mcs_proto::LoginRequest::ANDROID_ID);
+ login_request->set_auth_token(auth_token_str);
+ login_request->set_id(kLoginId);
+ login_request->set_domain(kLoginDomain);
+ login_request->set_device_id(kLoginDeviceIdPrefix + auth_id_hex);
+ login_request->set_network_type(1);
+ login_request->set_resource(auth_id_str);
+ login_request->set_user(auth_id_str);
+ login_request->set_use_rmq2(true);
+
+ login_request->add_setting();
+ login_request->mutable_setting(0)->set_name(kLoginSettingName);
+ login_request->mutable_setting(0)->set_value(kLoginSettingValue);
+ return login_request.Pass();
+}
+
+scoped_ptr<mcs_proto::IqStanza> BuildStreamAck() {
+ scoped_ptr<mcs_proto::IqStanza> stream_ack_iq(new mcs_proto::IqStanza());
+ stream_ack_iq->set_type(mcs_proto::IqStanza::SET);
+ stream_ack_iq->set_id("");
+ stream_ack_iq->mutable_extension()->set_id(kStreamAck);
+ stream_ack_iq->mutable_extension()->set_data("");
+ return stream_ack_iq.Pass();
+}
+
+scoped_ptr<mcs_proto::IqStanza> BuildSelectiveAck(
+ const std::vector<std::string>& acked_ids) {
+ scoped_ptr<mcs_proto::IqStanza> selective_ack_iq(new mcs_proto::IqStanza());
+ selective_ack_iq->set_type(mcs_proto::IqStanza::SET);
+ selective_ack_iq->set_id("");
+ selective_ack_iq->mutable_extension()->set_id(kSelectiveAck);
+ mcs_proto::SelectiveAck selective_ack;
+ for (size_t i = 0; i < acked_ids.size(); ++i)
+ selective_ack.add_id(acked_ids[i]);
+ selective_ack_iq->mutable_extension()->set_data(
+ selective_ack.SerializeAsString());
+ return selective_ack_iq.Pass();
+}
+
+// Utility method to build a google::protobuf::MessageLite object from a MCS
+// tag.
+scoped_ptr<google::protobuf::MessageLite> BuildProtobufFromTag(uint8 tag) {
+ switch(tag) {
+ case kHeartbeatPingTag:
+ return scoped_ptr<google::protobuf::MessageLite>(
+ new mcs_proto::HeartbeatPing());
+ case kHeartbeatAckTag:
+ return scoped_ptr<google::protobuf::MessageLite>(
+ new mcs_proto::HeartbeatAck());
+ case kLoginRequestTag:
+ return scoped_ptr<google::protobuf::MessageLite>(
+ new mcs_proto::LoginRequest());
+ case kLoginResponseTag:
+ return scoped_ptr<google::protobuf::MessageLite>(
+ new mcs_proto::LoginResponse());
+ case kCloseTag:
+ return scoped_ptr<google::protobuf::MessageLite>(
+ new mcs_proto::Close());
+ case kIqStanzaTag:
+ return scoped_ptr<google::protobuf::MessageLite>(
+ new mcs_proto::IqStanza());
+ case kDataMessageStanzaTag:
+ return scoped_ptr<google::protobuf::MessageLite>(
+ new mcs_proto::DataMessageStanza());
+ case kStreamErrorStanzaTag:
+ return scoped_ptr<google::protobuf::MessageLite>(
+ new mcs_proto::StreamErrorStanza());
+ default:
+ return scoped_ptr<google::protobuf::MessageLite>();
+ }
+}
+
+// Utility method to extract a MCS tag from a google::protobuf::MessageLite
+// object.
+int GetMCSProtoTag(const google::protobuf::MessageLite& message) {
+ const std::string& type_name = message.GetTypeName();
+ if (type_name == kProtoNames[kHeartbeatPingTag]) {
+ return kHeartbeatPingTag;
+ } else if (type_name == kProtoNames[kHeartbeatAckTag]) {
+ return kHeartbeatAckTag;
+ } else if (type_name == kProtoNames[kLoginRequestTag]) {
+ return kLoginRequestTag;
+ } else if (type_name == kProtoNames[kLoginResponseTag]) {
+ return kLoginResponseTag;
+ } else if (type_name == kProtoNames[kCloseTag]) {
+ return kCloseTag;
+ } else if (type_name == kProtoNames[kIqStanzaTag]) {
+ return kIqStanzaTag;
+ } else if (type_name == kProtoNames[kDataMessageStanzaTag]) {
+ return kDataMessageStanzaTag;
+ } else if (type_name == kProtoNames[kStreamErrorStanzaTag]) {
+ return kStreamErrorStanzaTag;
+ }
+ return -1;
+}
+
+std::string GetPersistentId(const google::protobuf::MessageLite& protobuf) {
+ if (protobuf.GetTypeName() == kProtoNames[kIqStanzaTag]) {
+ return reinterpret_cast<const mcs_proto::IqStanza*>(&protobuf)->
+ persistent_id();
+ } else if (protobuf.GetTypeName() == kProtoNames[kDataMessageStanzaTag]) {
+ return reinterpret_cast<const mcs_proto::DataMessageStanza*>(&protobuf)->
+ persistent_id();
+ }
+ // Not all message types have persistent ids. Just return empty string;
+ return "";
+}
+
+void SetPersistentId(const std::string& persistent_id,
+ google::protobuf::MessageLite* protobuf) {
+ if (protobuf->GetTypeName() == kProtoNames[kIqStanzaTag]) {
+ reinterpret_cast<mcs_proto::IqStanza*>(protobuf)->
+ set_persistent_id(persistent_id);
+ return;
+ } else if (protobuf->GetTypeName() == kProtoNames[kDataMessageStanzaTag]) {
+ reinterpret_cast<mcs_proto::DataMessageStanza*>(protobuf)->
+ set_persistent_id(persistent_id);
+ return;
+ }
+ NOTREACHED();
+}
+
+uint32 GetLastStreamIdReceived(const google::protobuf::MessageLite& protobuf) {
+ if (protobuf.GetTypeName() == kProtoNames[kIqStanzaTag]) {
+ return reinterpret_cast<const mcs_proto::IqStanza*>(&protobuf)->
+ last_stream_id_received();
+ } else if (protobuf.GetTypeName() == kProtoNames[kDataMessageStanzaTag]) {
+ return reinterpret_cast<const mcs_proto::DataMessageStanza*>(&protobuf)->
+ last_stream_id_received();
+ } else if (protobuf.GetTypeName() == kProtoNames[kHeartbeatPingTag]) {
+ return reinterpret_cast<const mcs_proto::HeartbeatPing*>(&protobuf)->
+ last_stream_id_received();
+ } else if (protobuf.GetTypeName() == kProtoNames[kHeartbeatAckTag]) {
+ return reinterpret_cast<const mcs_proto::HeartbeatAck*>(&protobuf)->
+ last_stream_id_received();
+ } else if (protobuf.GetTypeName() == kProtoNames[kLoginResponseTag]) {
+ return reinterpret_cast<const mcs_proto::LoginResponse*>(&protobuf)->
+ last_stream_id_received();
+ }
+ // Not all message types have last stream ids. Just return 0.
+ return 0;
+}
+
+void SetLastStreamIdReceived(uint32 val,
+ google::protobuf::MessageLite* protobuf) {
+ if (protobuf->GetTypeName() == kProtoNames[kIqStanzaTag]) {
+ reinterpret_cast<mcs_proto::IqStanza*>(protobuf)->
+ set_last_stream_id_received(val);
+ return;
+ } else if (protobuf->GetTypeName() == kProtoNames[kHeartbeatPingTag]) {
+ reinterpret_cast<mcs_proto::HeartbeatPing*>(protobuf)->
+ set_last_stream_id_received(val);
+ return;
+ } else if (protobuf->GetTypeName() == kProtoNames[kHeartbeatAckTag]) {
+ reinterpret_cast<mcs_proto::HeartbeatAck*>(protobuf)->
+ set_last_stream_id_received(val);
+ return;
+ } else if (protobuf->GetTypeName() == kProtoNames[kDataMessageStanzaTag]) {
+ reinterpret_cast<mcs_proto::DataMessageStanza*>(protobuf)->
+ set_last_stream_id_received(val);
+ return;
+ } else if (protobuf->GetTypeName() == kProtoNames[kLoginResponseTag]) {
+ reinterpret_cast<mcs_proto::LoginResponse*>(protobuf)->
+ set_last_stream_id_received(val);
+ return;
+ }
+ NOTREACHED();
+}
+
+} // namespace gcm
diff --git a/chromium/google_apis/gcm/base/mcs_util.h b/chromium/google_apis/gcm/base/mcs_util.h
new file mode 100644
index 00000000000..7f92564ad27
--- /dev/null
+++ b/chromium/google_apis/gcm/base/mcs_util.h
@@ -0,0 +1,81 @@
+// Copyright 2013 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+//
+// Utility methods for MCS interactions.
+
+#ifndef GOOGLE_APIS_GCM_BASE_MCS_UTIL_H_
+#define GOOGLE_APIS_GCM_BASE_MCS_UTIL_H_
+
+#include <string>
+
+#include "base/basictypes.h"
+#include "base/memory/ref_counted.h"
+#include "base/memory/scoped_ptr.h"
+#include "google_apis/gcm/base/gcm_export.h"
+#include "google_apis/gcm/protocol/mcs.pb.h"
+
+namespace net {
+class StreamSocket;
+}
+
+namespace gcm {
+
+// MCS Message tags.
+// WARNING: the order of these tags must remain the same, as the tag values
+// must be consistent with those used on the server.
+enum MCSProtoTag {
+ kHeartbeatPingTag = 0,
+ kHeartbeatAckTag,
+ kLoginRequestTag,
+ kLoginResponseTag,
+ kCloseTag,
+ kMessageStanzaTag,
+ kPresenceStanzaTag,
+ kIqStanzaTag,
+ kDataMessageStanzaTag,
+ kBatchPresenceStanzaTag,
+ kStreamErrorStanzaTag,
+ kHttpRequestTag,
+ kHttpResponseTag,
+ kBindAccountRequestTag,
+ kBindAccountResponseTag,
+ kTalkMetadataTag,
+ kNumProtoTypes,
+};
+
+enum MCSIqStanzaExtension {
+ kSelectiveAck = 12,
+ kStreamAck = 13,
+};
+
+// Builds a LoginRequest with the hardcoded local data.
+GCM_EXPORT scoped_ptr<mcs_proto::LoginRequest> BuildLoginRequest(
+ uint64 auth_id,
+ uint64 auth_token);
+
+// Builds a StreamAck IqStanza message.
+GCM_EXPORT scoped_ptr<mcs_proto::IqStanza> BuildStreamAck();
+GCM_EXPORT scoped_ptr<mcs_proto::IqStanza> BuildSelectiveAck(
+ const std::vector<std::string>& acked_ids);
+
+// Utility methods for building and identifying MCS protobufs.
+GCM_EXPORT scoped_ptr<google::protobuf::MessageLite>
+ BuildProtobufFromTag(uint8 tag);
+GCM_EXPORT int GetMCSProtoTag(const google::protobuf::MessageLite& message);
+
+// RMQ utility methods for extracting/setting common data from/to protobufs.
+GCM_EXPORT std::string GetPersistentId(
+ const google::protobuf::MessageLite& message);
+GCM_EXPORT void SetPersistentId(
+ const std::string& persistent_id,
+ google::protobuf::MessageLite* message);
+GCM_EXPORT uint32 GetLastStreamIdReceived(
+ const google::protobuf::MessageLite& protobuf);
+GCM_EXPORT void SetLastStreamIdReceived(
+ uint32 last_stream_id_received,
+ google::protobuf::MessageLite* protobuf);
+
+} // namespace gcm
+
+#endif // GOOGLE_APIS_GCM_BASE_MCS_UTIL_H_
diff --git a/chromium/google_apis/gcm/base/mcs_util_unittest.cc b/chromium/google_apis/gcm/base/mcs_util_unittest.cc
new file mode 100644
index 00000000000..d25914583a4
--- /dev/null
+++ b/chromium/google_apis/gcm/base/mcs_util_unittest.cc
@@ -0,0 +1,82 @@
+// Copyright 2013 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "google_apis/gcm/base/mcs_util.h"
+
+#include "base/bind.h"
+#include "base/memory/scoped_ptr.h"
+#include "base/run_loop.h"
+#include "base/strings/string_number_conversions.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+namespace gcm {
+namespace {
+
+const uint64 kAuthId = 4421448356646222460;
+const uint64 kAuthToken = 12345;
+
+// Build a login request protobuf.
+TEST(MCSUtilTest, BuildLoginRequest) {
+ scoped_ptr<mcs_proto::LoginRequest> login_request =
+ BuildLoginRequest(kAuthId, kAuthToken);
+ ASSERT_EQ("login-1", login_request->id());
+ ASSERT_EQ(base::Uint64ToString(kAuthToken), login_request->auth_token());
+ ASSERT_EQ(base::Uint64ToString(kAuthId), login_request->user());
+ ASSERT_EQ("android-3d5c23dac2a1fa7c", login_request->device_id());
+ // TODO(zea): test the other fields once they have valid values.
+}
+
+// Test building a protobuf and extracting the tag from a protobuf.
+TEST(MCSUtilTest, ProtobufToTag) {
+ for (size_t i = 0; i < kNumProtoTypes; ++i) {
+ scoped_ptr<google::protobuf::MessageLite> protobuf =
+ BuildProtobufFromTag(i);
+ if (!protobuf.get()) // Not all tags have protobuf definitions.
+ continue;
+ ASSERT_EQ((int)i, GetMCSProtoTag(*protobuf)) << "Type " << i;
+ }
+}
+
+// Test getting and setting persistent ids.
+TEST(MCSUtilTest, PersistentIds) {
+ COMPILE_ASSERT(kNumProtoTypes == 16U, UpdatePersistentIds);
+ const int kTagsWithPersistentIds[] = {
+ kIqStanzaTag,
+ kDataMessageStanzaTag
+ };
+ for (size_t i = 0; i < arraysize(kTagsWithPersistentIds); ++i) {
+ int tag = kTagsWithPersistentIds[i];
+ scoped_ptr<google::protobuf::MessageLite> protobuf =
+ BuildProtobufFromTag(tag);
+ ASSERT_TRUE(protobuf.get());
+ SetPersistentId(base::IntToString(tag), protobuf.get());
+ int get_val = 0;
+ base::StringToInt(GetPersistentId(*protobuf), &get_val);
+ ASSERT_EQ(tag, get_val);
+ }
+}
+
+// Test getting and setting stream ids.
+TEST(MCSUtilTest, StreamIds) {
+ COMPILE_ASSERT(kNumProtoTypes == 16U, UpdateStreamIds);
+ const int kTagsWithStreamIds[] = {
+ kIqStanzaTag,
+ kDataMessageStanzaTag,
+ kHeartbeatPingTag,
+ kHeartbeatAckTag,
+ kLoginResponseTag,
+ };
+ for (size_t i = 0; i < arraysize(kTagsWithStreamIds); ++i) {
+ int tag = kTagsWithStreamIds[i];
+ scoped_ptr<google::protobuf::MessageLite> protobuf =
+ BuildProtobufFromTag(tag);
+ ASSERT_TRUE(protobuf.get());
+ SetLastStreamIdReceived(tag, protobuf.get());
+ int get_id = GetLastStreamIdReceived(*protobuf);
+ ASSERT_EQ(tag, get_id);
+ }
+}
+
+} // namespace
+} // namespace gcm
diff --git a/chromium/google_apis/gcm/base/socket_stream.cc b/chromium/google_apis/gcm/base/socket_stream.cc
new file mode 100644
index 00000000000..1a0b29d8d07
--- /dev/null
+++ b/chromium/google_apis/gcm/base/socket_stream.cc
@@ -0,0 +1,332 @@
+// Copyright 2013 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "google_apis/gcm/base/socket_stream.h"
+
+#include "base/bind.h"
+#include "base/callback.h"
+#include "net/base/io_buffer.h"
+#include "net/socket/stream_socket.h"
+
+namespace gcm {
+
+namespace {
+
+// TODO(zea): consider having dynamically-sized buffers if this becomes too
+// expensive.
+const uint32 kDefaultBufferSize = 8*1024;
+
+} // namespace
+
+SocketInputStream::SocketInputStream(net::StreamSocket* socket)
+ : socket_(socket),
+ io_buffer_(new net::IOBuffer(kDefaultBufferSize)),
+ read_buffer_(new net::DrainableIOBuffer(io_buffer_.get(),
+ kDefaultBufferSize)),
+ next_pos_(0),
+ last_error_(net::OK),
+ weak_ptr_factory_(this) {
+ DCHECK(socket->IsConnected());
+}
+
+SocketInputStream::~SocketInputStream() {
+}
+
+bool SocketInputStream::Next(const void** data, int* size) {
+ if (GetState() != EMPTY && GetState() != READY) {
+ NOTREACHED() << "Invalid input stream read attempt.";
+ return false;
+ }
+
+ if (GetState() == EMPTY) {
+ DVLOG(1) << "No unread data remaining, ending read.";
+ return false;
+ }
+
+ DCHECK_EQ(GetState(), READY)
+ << " Input stream must have pending data before reading.";
+ DCHECK_LT(next_pos_, read_buffer_->BytesConsumed());
+ *data = io_buffer_->data() + next_pos_;
+ *size = UnreadByteCount();
+ next_pos_ = read_buffer_->BytesConsumed();
+ DVLOG(1) << "Consuming " << *size << " bytes in input buffer.";
+ return true;
+}
+
+void SocketInputStream::BackUp(int count) {
+ DCHECK(GetState() == READY || GetState() == EMPTY);
+ DCHECK_GT(count, 0);
+ DCHECK_LE(count, next_pos_);
+
+ next_pos_ -= count;
+ DVLOG(1) << "Backing up " << count << " bytes in input buffer. "
+ << "Current position now at " << next_pos_
+ << " of " << read_buffer_->BytesConsumed();
+}
+
+bool SocketInputStream::Skip(int count) {
+ NOTIMPLEMENTED();
+ return false;
+}
+
+int64 SocketInputStream::ByteCount() const {
+ DCHECK_NE(GetState(), CLOSED);
+ DCHECK_NE(GetState(), READING);
+ return next_pos_;
+}
+
+size_t SocketInputStream::UnreadByteCount() const {
+ DCHECK_NE(GetState(), CLOSED);
+ DCHECK_NE(GetState(), READING);
+ return read_buffer_->BytesConsumed() - next_pos_;
+}
+
+net::Error SocketInputStream::Refresh(const base::Closure& callback,
+ int byte_limit) {
+ DCHECK_NE(GetState(), CLOSED);
+ DCHECK_NE(GetState(), READING);
+ DCHECK_GT(byte_limit, 0);
+
+ if (byte_limit > read_buffer_->BytesRemaining()) {
+ NOTREACHED() << "Out of buffer space, closing input stream.";
+ CloseStream(net::ERR_UNEXPECTED, base::Closure());
+ return net::OK;
+ }
+
+ if (!socket_->IsConnected()) {
+ LOG(ERROR) << "Socket was disconnected, closing input stream";
+ CloseStream(net::ERR_CONNECTION_CLOSED, base::Closure());
+ return net::OK;
+ }
+
+ DVLOG(1) << "Refreshing input stream, limit of " << byte_limit << " bytes.";
+ int result = socket_->Read(
+ read_buffer_,
+ byte_limit,
+ base::Bind(&SocketInputStream::RefreshCompletionCallback,
+ weak_ptr_factory_.GetWeakPtr(),
+ callback));
+ DVLOG(1) << "Read returned " << result;
+ if (result == net::ERR_IO_PENDING) {
+ last_error_ = net::ERR_IO_PENDING;
+ return net::ERR_IO_PENDING;
+ }
+
+ RefreshCompletionCallback(base::Closure(), result);
+ return net::OK;
+}
+
+void SocketInputStream::RebuildBuffer() {
+ DVLOG(1) << "Rebuilding input stream, consumed "
+ << next_pos_ << " bytes.";
+ DCHECK_NE(GetState(), READING);
+ DCHECK_NE(GetState(), CLOSED);
+
+ int unread_data_size = 0;
+ const void* unread_data_ptr = NULL;
+ Next(&unread_data_ptr, &unread_data_size);
+ ResetInternal();
+
+ if (unread_data_ptr != io_buffer_->data()) {
+ DVLOG(1) << "Have " << unread_data_size
+ << " unread bytes remaining, shifting.";
+ // Move any remaining unread data to the start of the buffer;
+ std::memmove(io_buffer_->data(), unread_data_ptr, unread_data_size);
+ } else {
+ DVLOG(1) << "Have " << unread_data_size << " unread bytes remaining.";
+ }
+ read_buffer_->DidConsume(unread_data_size);
+}
+
+net::Error SocketInputStream::last_error() const {
+ return last_error_;
+}
+
+SocketInputStream::State SocketInputStream::GetState() const {
+ if (last_error_ < net::ERR_IO_PENDING)
+ return CLOSED;
+
+ if (last_error_ == net::ERR_IO_PENDING)
+ return READING;
+
+ DCHECK_EQ(last_error_, net::OK);
+ if (read_buffer_->BytesConsumed() == next_pos_)
+ return EMPTY;
+
+ return READY;
+}
+
+void SocketInputStream::RefreshCompletionCallback(
+ const base::Closure& callback, int result) {
+ // If an error occurred before the completion callback could complete, ignore
+ // the result.
+ if (GetState() == CLOSED)
+ return;
+
+ // Result == 0 implies EOF, which is treated as an error.
+ if (result == 0)
+ result = net::ERR_CONNECTION_CLOSED;
+
+ DCHECK_NE(result, net::ERR_IO_PENDING);
+
+ if (result < net::OK) {
+ DVLOG(1) << "Failed to refresh socket: " << result;
+ CloseStream(static_cast<net::Error>(result), callback);
+ return;
+ }
+
+ DCHECK_GT(result, 0);
+ last_error_ = net::OK;
+ read_buffer_->DidConsume(result);
+
+ DVLOG(1) << "Refresh complete with " << result << " new bytes. "
+ << "Current position " << next_pos_
+ << " of " << read_buffer_->BytesConsumed() << ".";
+
+ if (!callback.is_null())
+ callback.Run();
+}
+
+void SocketInputStream::ResetInternal() {
+ read_buffer_->SetOffset(0);
+ next_pos_ = 0;
+ last_error_ = net::OK;
+ weak_ptr_factory_.InvalidateWeakPtrs(); // Invalidate any callbacks.
+}
+
+void SocketInputStream::CloseStream(net::Error error,
+ const base::Closure& callback) {
+ DCHECK_LT(error, net::ERR_IO_PENDING);
+ ResetInternal();
+ last_error_ = error;
+ LOG(ERROR) << "Closing stream with result " << error;
+ if (!callback.is_null())
+ callback.Run();
+}
+
+SocketOutputStream::SocketOutputStream(net::StreamSocket* socket)
+ : socket_(socket),
+ io_buffer_(new net::IOBuffer(kDefaultBufferSize)),
+ write_buffer_(new net::DrainableIOBuffer(io_buffer_.get(),
+ kDefaultBufferSize)),
+ next_pos_(0),
+ last_error_(net::OK),
+ weak_ptr_factory_(this) {
+ DCHECK(socket->IsConnected());
+}
+
+SocketOutputStream::~SocketOutputStream() {
+}
+
+bool SocketOutputStream::Next(void** data, int* size) {
+ DCHECK_NE(GetState(), CLOSED);
+ DCHECK_NE(GetState(), FLUSHING);
+ if (next_pos_ == write_buffer_->size())
+ return false;
+
+ *data = write_buffer_->data() + next_pos_;
+ *size = write_buffer_->size() - next_pos_;
+ next_pos_ = write_buffer_->size();
+ return true;
+}
+
+void SocketOutputStream::BackUp(int count) {
+ DCHECK_GE(count, 0);
+ if (count > next_pos_)
+ next_pos_ = 0;
+ next_pos_ -= count;
+ DVLOG(1) << "Backing up " << count << " bytes in output buffer. "
+ << next_pos_ << " bytes used.";
+}
+
+int64 SocketOutputStream::ByteCount() const {
+ DCHECK_NE(GetState(), CLOSED);
+ DCHECK_NE(GetState(), FLUSHING);
+ return next_pos_;
+}
+
+net::Error SocketOutputStream::Flush(const base::Closure& callback) {
+ DCHECK_EQ(GetState(), READY);
+
+ if (!socket_->IsConnected()) {
+ LOG(ERROR) << "Socket was disconnected, closing output stream";
+ last_error_ = net::ERR_CONNECTION_CLOSED;
+ return net::OK;
+ }
+
+ DVLOG(1) << "Flushing " << next_pos_ << " bytes into socket.";
+ int result = socket_->Write(
+ write_buffer_,
+ next_pos_,
+ base::Bind(&SocketOutputStream::FlushCompletionCallback,
+ weak_ptr_factory_.GetWeakPtr(),
+ callback));
+ DVLOG(1) << "Write returned " << result;
+ if (result == net::ERR_IO_PENDING) {
+ last_error_ = net::ERR_IO_PENDING;
+ return net::ERR_IO_PENDING;
+ }
+
+ FlushCompletionCallback(base::Closure(), result);
+ return net::OK;
+}
+
+SocketOutputStream::State SocketOutputStream::GetState() const{
+ if (last_error_ < net::ERR_IO_PENDING)
+ return CLOSED;
+
+ if (last_error_ == net::ERR_IO_PENDING)
+ return FLUSHING;
+
+ DCHECK_EQ(last_error_, net::OK);
+ if (next_pos_ == 0)
+ return EMPTY;
+
+ return READY;
+}
+
+net::Error SocketOutputStream::last_error() const {
+ return last_error_;
+}
+
+void SocketOutputStream::FlushCompletionCallback(
+ const base::Closure& callback, int result) {
+ // If an error occurred before the completion callback could complete, ignore
+ // the result.
+ if (GetState() == CLOSED)
+ return;
+
+ // Result == 0 implies EOF, which is treated as an error.
+ if (result == 0)
+ result = net::ERR_CONNECTION_CLOSED;
+
+ DCHECK_NE(result, net::ERR_IO_PENDING);
+
+ if (result < net::OK) {
+ LOG(ERROR) << "Failed to flush socket.";
+ last_error_ = static_cast<net::Error>(result);
+ if (!callback.is_null())
+ callback.Run();
+ return;
+ }
+
+ DCHECK_GT(result, net::OK);
+ last_error_ = net::OK;
+
+ if (write_buffer_->BytesConsumed() + result < next_pos_) {
+ DVLOG(1) << "Partial flush complete. Retrying.";
+ // Only a partial write was completed. Flush again to finish the write.
+ write_buffer_->DidConsume(result);
+ Flush(callback);
+ return;
+ }
+
+ DVLOG(1) << "Socket flush complete.";
+ write_buffer_->SetOffset(0);
+ next_pos_ = 0;
+ if (!callback.is_null())
+ callback.Run();
+}
+
+} // namespace gcm
diff --git a/chromium/google_apis/gcm/base/socket_stream.h b/chromium/google_apis/gcm/base/socket_stream.h
new file mode 100644
index 00000000000..a45842016f6
--- /dev/null
+++ b/chromium/google_apis/gcm/base/socket_stream.h
@@ -0,0 +1,205 @@
+// Copyright 2013 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+//
+// Protobuf ZeroCopy[Input/Output]Stream implementations capable of using a
+// net::StreamSocket. Built to work with Protobuf CodedStreams.
+
+#ifndef GOOGLE_APIS_GCM_BASE_SOCKET_STREAM_H_
+#define GOOGLE_APIS_GCM_BASE_SOCKET_STREAM_H_
+
+#include "base/basictypes.h"
+#include "base/callback_forward.h"
+#include "base/compiler_specific.h"
+#include "base/memory/ref_counted.h"
+#include "base/memory/weak_ptr.h"
+#include "google/protobuf/io/zero_copy_stream.h"
+#include "google_apis/gcm/base/gcm_export.h"
+#include "net/base/net_errors.h"
+
+namespace net {
+class DrainableIOBuffer;
+class IOBuffer;
+class StreamSocket;
+} // namespace net
+
+namespace gcm {
+
+// A helper class for interacting with a net::StreamSocket that is receiving
+// protobuf encoded messages. A SocketInputStream does not take ownership of
+// the socket itself, and it is expected that the life of the input stream
+// should match the life of the socket itself (while the socket remains
+// connected). If an error is encounters, the input stream will store the error
+// in |last_error_|, and GetState() will be set to CLOSED.
+// Typical usage:
+// 1. Check the GetState() of the input stream before using it. If CLOSED, the
+// input stream must be rebuilt (and the socket likely needs to be
+// reconnected as an error was encountered).
+// 2. If GetState() is EMPTY, call Refresh(..), passing the maximum byte size
+// for a message, and wait until completion. It is invalid to attempt to
+// Refresh an input stream or read data from the stream while a Refresh is
+// pending.
+// 3. Check GetState() again to ensure the Refresh was successful.
+// 4. Use a CodedInputStream to read from the ZeroCopyInputStream interface of
+// the SocketInputStream. Next(..) will return true until there is no data
+// remaining.
+// 5. Call RebuildBuffer when done reading, to shift any unread data to the
+// start of the buffer.
+// 6. Repeat as necessary.
+class GCM_EXPORT SocketInputStream
+ : public google::protobuf::io::ZeroCopyInputStream {
+ public:
+ enum State {
+ // No valid data to read. This means the buffer is either empty or all data
+ // in the buffer has already been consumed.
+ EMPTY,
+ // Valid data to read.
+ READY,
+ // In the process of reading new data from the socket.
+ READING,
+ // An permanent error occurred and the stream is now closed.
+ CLOSED,
+ };
+
+ // |socket| should already be connected.
+ explicit SocketInputStream(net::StreamSocket* socket);
+ virtual ~SocketInputStream();
+
+ // ZeroCopyInputStream implementation.
+ virtual bool Next(const void** data, int* size) OVERRIDE;
+ virtual void BackUp(int count) OVERRIDE;
+ virtual bool Skip(int count) OVERRIDE; // Not implemented.
+ virtual int64 ByteCount() const OVERRIDE;
+
+ // The remaining amount of valid data available to be read.
+ size_t UnreadByteCount() const;
+
+ // Reads from the socket, appending a max of |byte_limit| bytes onto the read
+ // buffer. net::ERR_IO_PENDING is returned if the refresh can't complete
+ // synchronously, in which case the callback is invoked upon completion. If
+ // the refresh can complete synchronously, even in case of an error, returns
+ // net::OK without invoking callback.
+ // Note: GetState() (and possibly last_error()) should be checked upon
+ // completion to determine whether the Refresh encountered an error.
+ net::Error Refresh(const base::Closure& callback, int byte_limit);
+
+ // Rebuilds the buffer state by copying over any unread data to the beginning
+ // of the buffer and resetting the buffer read/write positions.
+ // Note: it is not valid to call Rebuild() if GetState() == CLOSED. The stream
+ // must be recreated from scratch in such a scenario.
+ void RebuildBuffer();
+
+ // Returns the last fatal error encountered. Only valid if GetState() ==
+ // CLOSED.
+ net::Error last_error() const;
+
+ // Returns the current state.
+ State GetState() const;
+
+ private:
+ // Clears the local state.
+ void ResetInternal();
+
+ // Callback for Socket::Read calls.
+ void RefreshCompletionCallback(const base::Closure& callback, int result);
+
+ // Permanently closes the stream.
+ void CloseStream(net::Error error, const base::Closure& callback);
+
+ // Internal net components.
+ net::StreamSocket* const socket_;
+ const scoped_refptr<net::IOBuffer> io_buffer_;
+ // IOBuffer implementation that wraps the data within |io_buffer_| that hasn't
+ // been written to yet by Socket::Read calls.
+ const scoped_refptr<net::DrainableIOBuffer> read_buffer_;
+
+ // Starting position of the data within |io_buffer_| to consume on subsequent
+ // Next(..) call. 0 <= next_pos_ <= read_buffer_.BytesConsumed()
+ // Note: next_pos == read_buffer_.BytesConsumed() implies GetState() == EMPTY.
+ int next_pos_;
+
+ // If < net::ERR_IO_PENDING, the last net error received.
+ // Note: last_error_ == net::ERR_IO_PENDING implies GetState() == READING.
+ net::Error last_error_;
+
+ base::WeakPtrFactory<SocketInputStream> weak_ptr_factory_;
+
+ DISALLOW_COPY_AND_ASSIGN(SocketInputStream);
+};
+
+// A helper class for writing to a SocketStream with protobuf encoded data.
+// A SocketOutputStream does not take ownership of the socket itself, and it is
+// expected that the life of the output stream should match the life of the
+// socket itself (while the socket remains connected).
+// Typical usage:
+// 1. Check the GetState() of the output stream before using it. If CLOSED, the
+// output stream must be rebuilt (and the socket likely needs to be
+// reconnected, as an error was encountered).
+// 2. If EMPTY, the output stream can be written via a CodedOutputStream using
+// the ZeroCopyOutputStream interface.
+// 3. Once done writing, GetState() should be READY, so call Flush(..) to write
+// the buffer into the StreamSocket. Wait for the callback to be invoked
+// (it's invalid to write to an output stream while it's flushing).
+// 4. Check the GetState() again to ensure the Flush was successful. GetState()
+// should be EMPTY again.
+// 5. Repeat.
+class GCM_EXPORT SocketOutputStream
+ : public google::protobuf::io::ZeroCopyOutputStream {
+ public:
+ enum State {
+ // No valid data yet.
+ EMPTY,
+ // Ready for flushing (some data is present).
+ READY,
+ // In the process of flushing into the socket.
+ FLUSHING,
+ // A permanent error occurred, and the stream is now closed.
+ CLOSED,
+ };
+
+ // |socket| should already be connected.
+ explicit SocketOutputStream(net::StreamSocket* socket);
+ virtual ~SocketOutputStream();
+
+ // ZeroCopyOutputStream implementation.
+ virtual bool Next(void** data, int* size) OVERRIDE;
+ virtual void BackUp(int count) OVERRIDE;
+ virtual int64 ByteCount() const OVERRIDE;
+
+ // Writes the buffer into the Socket.
+ net::Error Flush(const base::Closure& callback);
+
+ // Returns the last fatal error encountered. Only valid if GetState() ==
+ // CLOSED.
+ net::Error last_error() const;
+
+ // Returns the current state.
+ State GetState() const;
+
+ private:
+ void FlushCompletionCallback(const base::Closure& callback, int result);
+
+ // Internal net components.
+ net::StreamSocket* const socket_;
+ const scoped_refptr<net::IOBuffer> io_buffer_;
+ // IOBuffer implementation that wraps the data within |io_buffer_| that hasn't
+ // been written to the socket yet.
+ const scoped_refptr<net::DrainableIOBuffer> write_buffer_;
+
+ // Starting position of the data within |io_buffer_| to consume on subsequent
+ // Next(..) call. 0 <= write_buffer_.BytesConsumed() <= next_pos_
+ // Note: next_pos == 0 implies GetState() == EMPTY.
+ int next_pos_;
+
+ // If < net::ERR_IO_PENDING, the last net error received.
+ // Note: last_error_ == net::ERR_IO_PENDING implies GetState() == FLUSHING.
+ net::Error last_error_;
+
+ base::WeakPtrFactory<SocketOutputStream> weak_ptr_factory_;
+
+ DISALLOW_COPY_AND_ASSIGN(SocketOutputStream);
+};
+
+} // namespace gcm
+
+#endif // GOOGLE_APIS_GCM_BASE_SOCKET_STREAM_H_
diff --git a/chromium/google_apis/gcm/base/socket_stream_unittest.cc b/chromium/google_apis/gcm/base/socket_stream_unittest.cc
new file mode 100644
index 00000000000..d7ba6793bf9
--- /dev/null
+++ b/chromium/google_apis/gcm/base/socket_stream_unittest.cc
@@ -0,0 +1,406 @@
+// Copyright 2013 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "google_apis/gcm/base/socket_stream.h"
+
+#include "base/basictypes.h"
+#include "base/bind.h"
+#include "base/memory/scoped_ptr.h"
+#include "base/run_loop.h"
+#include "base/stl_util.h"
+#include "base/strings/string_piece.h"
+#include "net/socket/socket_test_util.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+namespace gcm {
+namespace {
+
+typedef std::vector<net::MockRead> ReadList;
+typedef std::vector<net::MockWrite> WriteList;
+
+const char kReadData[] = "read_data";
+const uint64 kReadDataSize = arraysize(kReadData) - 1;
+const char kReadData2[] = "read_alternate_data";
+const uint64 kReadData2Size = arraysize(kReadData2) - 1;
+const char kWriteData[] = "write_data";
+const uint64 kWriteDataSize = arraysize(kWriteData) - 1;
+
+class GCMSocketStreamTest : public testing::Test {
+ public:
+ GCMSocketStreamTest();
+ virtual ~GCMSocketStreamTest();
+
+ // Build a socket with the expected reads and writes.
+ void BuildSocket(const ReadList& read_list, const WriteList& write_list);
+
+ // Pump the message loop until idle.
+ void PumpLoop();
+
+ // Simulates a google::protobuf::io::CodedInputStream read.
+ base::StringPiece DoInputStreamRead(uint64 bytes);
+ // Simulates a google::protobuf::io::CodedOutputStream write.
+ uint64 DoOutputStreamWrite(const base::StringPiece& write_src);
+
+ // Synchronous Refresh wrapper.
+ void WaitForData(size_t msg_size);
+
+ base::MessageLoop* message_loop() { return &message_loop_; };
+ net::DelayedSocketData* data_provider() { return data_provider_.get(); }
+ SocketInputStream* input_stream() { return socket_input_stream_.get(); }
+ SocketOutputStream* output_stream() { return socket_output_stream_.get(); }
+ net::StreamSocket* socket() { return socket_.get(); }
+
+ private:
+ void OpenConnection();
+ void ResetInputStream();
+ void ResetOutputStream();
+
+ void ConnectCallback(int result);
+
+ // SocketStreams and their data providers.
+ ReadList mock_reads_;
+ WriteList mock_writes_;
+ scoped_ptr<net::DelayedSocketData> data_provider_;
+ scoped_ptr<SocketInputStream> socket_input_stream_;
+ scoped_ptr<SocketOutputStream> socket_output_stream_;
+
+ // net:: components.
+ scoped_ptr<net::StreamSocket> socket_;
+ net::MockClientSocketFactory socket_factory_;
+ net::AddressList address_list_;
+
+ base::MessageLoopForIO message_loop_;
+};
+
+GCMSocketStreamTest::GCMSocketStreamTest() {
+ net::IPAddressNumber ip_number;
+ net::ParseIPLiteralToNumber("127.0.0.1", &ip_number);
+ address_list_ = net::AddressList::CreateFromIPAddress(ip_number, 5228);
+}
+
+GCMSocketStreamTest::~GCMSocketStreamTest() {}
+
+void GCMSocketStreamTest::BuildSocket(const ReadList& read_list,
+ const WriteList& write_list) {
+ mock_reads_ = read_list;
+ mock_writes_ = write_list;
+ data_provider_.reset(
+ new net::DelayedSocketData(
+ 0,
+ vector_as_array(&mock_reads_), mock_reads_.size(),
+ vector_as_array(&mock_writes_), mock_writes_.size()));
+ socket_factory_.AddSocketDataProvider(data_provider_.get());
+ OpenConnection();
+ ResetInputStream();
+ ResetOutputStream();
+}
+
+void GCMSocketStreamTest::PumpLoop() {
+ base::RunLoop run_loop;
+ run_loop.RunUntilIdle();
+}
+
+base::StringPiece GCMSocketStreamTest::DoInputStreamRead(uint64 bytes) {
+ uint64 total_bytes_read = 0;
+ const void* initial_buffer = NULL;
+ const void* buffer = NULL;
+ int size = 0;
+
+ do {
+ DCHECK(socket_input_stream_->GetState() == SocketInputStream::EMPTY ||
+ socket_input_stream_->GetState() == SocketInputStream::READY);
+ if (!socket_input_stream_->Next(&buffer, &size))
+ break;
+ total_bytes_read += size;
+ if (initial_buffer) { // Verify the buffer doesn't skip data.
+ EXPECT_EQ(static_cast<const uint8*>(initial_buffer) + total_bytes_read,
+ static_cast<const uint8*>(buffer) + size);
+ } else {
+ initial_buffer = buffer;
+ }
+ } while (total_bytes_read < bytes);
+
+ if (total_bytes_read > bytes) {
+ socket_input_stream_->BackUp(total_bytes_read - bytes);
+ total_bytes_read = bytes;
+ }
+
+ return base::StringPiece(static_cast<const char*>(initial_buffer),
+ total_bytes_read);
+}
+
+uint64 GCMSocketStreamTest::DoOutputStreamWrite(
+ const base::StringPiece& write_src) {
+ DCHECK_EQ(socket_output_stream_->GetState(), SocketOutputStream::EMPTY);
+ uint64 total_bytes_written = 0;
+ void* buffer = NULL;
+ int size = 0;
+ size_t bytes = write_src.size();
+
+ do {
+ if (!socket_output_stream_->Next(&buffer, &size))
+ break;
+ uint64 bytes_to_write = (static_cast<uint64>(size) < bytes ? size : bytes);
+ memcpy(buffer,
+ write_src.data() + total_bytes_written,
+ bytes_to_write);
+ if (bytes_to_write < static_cast<uint64>(size))
+ socket_output_stream_->BackUp(size - bytes_to_write);
+ total_bytes_written += bytes_to_write;
+ } while (total_bytes_written < bytes);
+
+ base::RunLoop run_loop;
+ if (socket_output_stream_->Flush(run_loop.QuitClosure()) ==
+ net::ERR_IO_PENDING) {
+ run_loop.Run();
+ }
+
+ return total_bytes_written;
+}
+
+void GCMSocketStreamTest::WaitForData(size_t msg_size) {
+ while (input_stream()->UnreadByteCount() < msg_size) {
+ base::RunLoop run_loop;
+ if (input_stream()->Refresh(run_loop.QuitClosure(),
+ msg_size - input_stream()->UnreadByteCount()) ==
+ net::ERR_IO_PENDING) {
+ run_loop.Run();
+ }
+ if (input_stream()->GetState() == SocketInputStream::CLOSED)
+ return;
+ }
+}
+
+void GCMSocketStreamTest::OpenConnection() {
+ socket_ = socket_factory_.CreateTransportClientSocket(
+ address_list_, NULL, net::NetLog::Source());
+ socket_->Connect(
+ base::Bind(&GCMSocketStreamTest::ConnectCallback,
+ base::Unretained(this)));
+ PumpLoop();
+}
+
+void GCMSocketStreamTest::ConnectCallback(int result) {}
+
+void GCMSocketStreamTest::ResetInputStream() {
+ DCHECK(socket_.get());
+ socket_input_stream_.reset(new SocketInputStream(socket_.get()));
+}
+
+void GCMSocketStreamTest::ResetOutputStream() {
+ DCHECK(socket_.get());
+ socket_output_stream_.reset(new SocketOutputStream(socket_.get()));
+}
+
+// A read where all data is already available.
+TEST_F(GCMSocketStreamTest, ReadDataSync) {
+ BuildSocket(ReadList(1, net::MockRead(net::SYNCHRONOUS,
+ kReadData,
+ kReadDataSize)),
+ WriteList());
+
+ WaitForData(kReadDataSize);
+ ASSERT_EQ(std::string(kReadData, kReadDataSize),
+ DoInputStreamRead(kReadDataSize));
+}
+
+// A read that comes in two parts.
+TEST_F(GCMSocketStreamTest, ReadPartialDataSync) {
+ size_t first_read_len = kReadDataSize / 2;
+ size_t second_read_len = kReadDataSize - first_read_len;
+ ReadList read_list;
+ read_list.push_back(
+ net::MockRead(net::SYNCHRONOUS,
+ kReadData,
+ first_read_len));
+ read_list.push_back(
+ net::MockRead(net::SYNCHRONOUS,
+ &kReadData[first_read_len],
+ second_read_len));
+ BuildSocket(read_list, WriteList());
+
+ WaitForData(kReadDataSize);
+ ASSERT_EQ(std::string(kReadData, kReadDataSize),
+ DoInputStreamRead(kReadDataSize));
+}
+
+// A read where no data is available at first (IO_PENDING will be returned).
+TEST_F(GCMSocketStreamTest, ReadAsync) {
+ size_t first_read_len = kReadDataSize / 2;
+ size_t second_read_len = kReadDataSize - first_read_len;
+ ReadList read_list;
+ read_list.push_back(
+ net::MockRead(net::SYNCHRONOUS, net::ERR_IO_PENDING));
+ read_list.push_back(
+ net::MockRead(net::ASYNC, kReadData, first_read_len));
+ read_list.push_back(
+ net::MockRead(net::ASYNC, &kReadData[first_read_len], second_read_len));
+ BuildSocket(read_list, WriteList());
+
+ base::MessageLoop::current()->PostTask(
+ FROM_HERE,
+ base::Bind(&net::DelayedSocketData::ForceNextRead,
+ base::Unretained(data_provider())));
+ WaitForData(kReadDataSize);
+ ASSERT_EQ(std::string(kReadData, kReadDataSize),
+ DoInputStreamRead(kReadDataSize));
+}
+
+// Simulate two packets arriving at once. Read them in two separate calls.
+TEST_F(GCMSocketStreamTest, TwoReadsAtOnce) {
+ std::string long_data = std::string(kReadData, kReadDataSize) +
+ std::string(kReadData2, kReadData2Size);
+ BuildSocket(ReadList(1, net::MockRead(net::SYNCHRONOUS,
+ long_data.c_str(),
+ long_data.size())),
+ WriteList());
+
+ WaitForData(kReadDataSize);
+ ASSERT_EQ(std::string(kReadData, kReadDataSize),
+ DoInputStreamRead(kReadDataSize));
+
+ WaitForData(kReadData2Size);
+ ASSERT_EQ(std::string(kReadData2, kReadData2Size),
+ DoInputStreamRead(kReadData2Size));
+}
+
+// Simulate two packets arriving at once. Read them in two calls separated
+// by a Rebuild.
+TEST_F(GCMSocketStreamTest, TwoReadsAtOnceWithRebuild) {
+ std::string long_data = std::string(kReadData, kReadDataSize) +
+ std::string(kReadData2, kReadData2Size);
+ BuildSocket(ReadList(1, net::MockRead(net::SYNCHRONOUS,
+ long_data.c_str(),
+ long_data.size())),
+ WriteList());
+
+ WaitForData(kReadDataSize);
+ ASSERT_EQ(std::string(kReadData, kReadDataSize),
+ DoInputStreamRead(kReadDataSize));
+
+ input_stream()->RebuildBuffer();
+ WaitForData(kReadData2Size);
+ ASSERT_EQ(std::string(kReadData2, kReadData2Size),
+ DoInputStreamRead(kReadData2Size));
+}
+
+// Simulate a read that is aborted.
+TEST_F(GCMSocketStreamTest, ReadError) {
+ int result = net::ERR_ABORTED;
+ BuildSocket(ReadList(1, net::MockRead(net::SYNCHRONOUS, result)),
+ WriteList());
+
+ WaitForData(kReadDataSize);
+ ASSERT_EQ(SocketInputStream::CLOSED, input_stream()->GetState());
+ ASSERT_EQ(result, input_stream()->last_error());
+}
+
+// Simulate a read after the connection is closed.
+TEST_F(GCMSocketStreamTest, ReadDisconnected) {
+ BuildSocket(ReadList(), WriteList());
+ socket()->Disconnect();
+ WaitForData(kReadDataSize);
+ ASSERT_EQ(SocketInputStream::CLOSED, input_stream()->GetState());
+ ASSERT_EQ(net::ERR_CONNECTION_CLOSED, input_stream()->last_error());
+}
+
+// Write a full message in one go.
+TEST_F(GCMSocketStreamTest, WriteFull) {
+ BuildSocket(ReadList(),
+ WriteList(1, net::MockWrite(net::SYNCHRONOUS,
+ kWriteData,
+ kWriteDataSize)));
+ ASSERT_EQ(kWriteDataSize,
+ DoOutputStreamWrite(base::StringPiece(kWriteData,
+ kWriteDataSize)));
+}
+
+// Write a message in two go's.
+TEST_F(GCMSocketStreamTest, WritePartial) {
+ WriteList write_list;
+ write_list.push_back(net::MockWrite(net::SYNCHRONOUS,
+ kWriteData,
+ kWriteDataSize / 2));
+ write_list.push_back(net::MockWrite(net::SYNCHRONOUS,
+ kWriteData + kWriteDataSize / 2,
+ kWriteDataSize / 2));
+ BuildSocket(ReadList(), write_list);
+ ASSERT_EQ(kWriteDataSize,
+ DoOutputStreamWrite(base::StringPiece(kWriteData,
+ kWriteDataSize)));
+}
+
+// Write a message completely asynchronously (returns IO_PENDING before
+// finishing the write in two go's).
+TEST_F(GCMSocketStreamTest, WriteNone) {
+ WriteList write_list;
+ write_list.push_back(net::MockWrite(net::SYNCHRONOUS,
+ kWriteData,
+ kWriteDataSize / 2));
+ write_list.push_back(net::MockWrite(net::SYNCHRONOUS,
+ kWriteData + kWriteDataSize / 2,
+ kWriteDataSize / 2));
+ BuildSocket(ReadList(), write_list);
+ ASSERT_EQ(kWriteDataSize,
+ DoOutputStreamWrite(base::StringPiece(kWriteData,
+ kWriteDataSize)));
+}
+
+// Write a message then read a message.
+TEST_F(GCMSocketStreamTest, WriteThenRead) {
+ BuildSocket(ReadList(1, net::MockRead(net::SYNCHRONOUS,
+ kReadData,
+ kReadDataSize)),
+ WriteList(1, net::MockWrite(net::SYNCHRONOUS,
+ kWriteData,
+ kWriteDataSize)));
+
+ ASSERT_EQ(kWriteDataSize,
+ DoOutputStreamWrite(base::StringPiece(kWriteData,
+ kWriteDataSize)));
+
+ WaitForData(kReadDataSize);
+ ASSERT_EQ(std::string(kReadData, kReadDataSize),
+ DoInputStreamRead(kReadDataSize));
+}
+
+// Read a message then write a message.
+TEST_F(GCMSocketStreamTest, ReadThenWrite) {
+ BuildSocket(ReadList(1, net::MockRead(net::SYNCHRONOUS,
+ kReadData,
+ kReadDataSize)),
+ WriteList(1, net::MockWrite(net::SYNCHRONOUS,
+ kWriteData,
+ kWriteDataSize)));
+
+ WaitForData(kReadDataSize);
+ ASSERT_EQ(std::string(kReadData, kReadDataSize),
+ DoInputStreamRead(kReadDataSize));
+
+ ASSERT_EQ(kWriteDataSize,
+ DoOutputStreamWrite(base::StringPiece(kWriteData,
+ kWriteDataSize)));
+}
+
+// Simulate a write that gets aborted.
+TEST_F(GCMSocketStreamTest, WriteError) {
+ int result = net::ERR_ABORTED;
+ BuildSocket(ReadList(),
+ WriteList(1, net::MockWrite(net::SYNCHRONOUS, result)));
+ DoOutputStreamWrite(base::StringPiece(kWriteData, kWriteDataSize));
+ ASSERT_EQ(SocketOutputStream::CLOSED, output_stream()->GetState());
+ ASSERT_EQ(result, output_stream()->last_error());
+}
+
+// Simulate a write after the connection is closed.
+TEST_F(GCMSocketStreamTest, WriteDisconnected) {
+ BuildSocket(ReadList(), WriteList());
+ socket()->Disconnect();
+ DoOutputStreamWrite(base::StringPiece(kWriteData, kWriteDataSize));
+ ASSERT_EQ(SocketOutputStream::CLOSED, output_stream()->GetState());
+ ASSERT_EQ(net::ERR_CONNECTION_CLOSED, output_stream()->last_error());
+}
+
+} // namespace
+} // namespace gcm
diff --git a/chromium/google_apis/gcm/engine/connection_factory.cc b/chromium/google_apis/gcm/engine/connection_factory.cc
new file mode 100644
index 00000000000..016e1e2b89c
--- /dev/null
+++ b/chromium/google_apis/gcm/engine/connection_factory.cc
@@ -0,0 +1,12 @@
+// Copyright 2013 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "google_apis/gcm/engine/connection_factory.h"
+
+namespace gcm {
+
+ConnectionFactory::ConnectionFactory() {}
+ConnectionFactory::~ConnectionFactory() {}
+
+} // namespace gcm
diff --git a/chromium/google_apis/gcm/engine/connection_factory.h b/chromium/google_apis/gcm/engine/connection_factory.h
new file mode 100644
index 00000000000..3cff48299b6
--- /dev/null
+++ b/chromium/google_apis/gcm/engine/connection_factory.h
@@ -0,0 +1,64 @@
+// Copyright 2013 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef GOOGLE_APIS_GCM_ENGINE_CONNECTION_FACTORY_H_
+#define GOOGLE_APIS_GCM_ENGINE_CONNECTION_FACTORY_H_
+
+#include "base/time/time.h"
+#include "google_apis/gcm/base/gcm_export.h"
+#include "google_apis/gcm/engine/connection_handler.h"
+
+namespace mcs_proto {
+class LoginRequest;
+}
+
+namespace gcm {
+
+// Factory for creating a ConnectionHandler and maintaining its connection.
+// The factory retains ownership of the ConnectionHandler and will enforce
+// backoff policies when attempting connections.
+class GCM_EXPORT ConnectionFactory {
+ public:
+ typedef base::Callback<void(mcs_proto::LoginRequest* login_request)>
+ BuildLoginRequestCallback;
+
+ ConnectionFactory();
+ virtual ~ConnectionFactory();
+
+ // Initialize the factory, creating a connection handler with a disconnected
+ // socket. Should only be called once.
+ // Upon connection:
+ // |read_callback| will be invoked with the contents of any received protobuf
+ // message.
+ // |write_callback| will be invoked anytime a message has been successfully
+ // sent. Note: this just means the data was sent to the wire, not that the
+ // other end received it.
+ virtual void Initialize(
+ const BuildLoginRequestCallback& request_builder,
+ const ConnectionHandler::ProtoReceivedCallback& read_callback,
+ const ConnectionHandler::ProtoSentCallback& write_callback) = 0;
+
+ // Get the connection handler for this factory. Initialize(..) must have
+ // been called.
+ virtual ConnectionHandler* GetConnectionHandler() const = 0;
+
+ // Opens a new connection and initiates login handshake. Upon completion of
+ // the handshake, |read_callback| will be invoked with a valid
+ // mcs_proto::LoginResponse.
+ // Note: Initialize must have already been invoked.
+ virtual void Connect() = 0;
+
+ // Whether or not the MCS endpoint is currently reachable with an active
+ // connection.
+ virtual bool IsEndpointReachable() const = 0;
+
+ // If in backoff, the time at which the next retry will be made. Otherwise,
+ // a null time, indicating either no attempt to connect has been made or no
+ // backoff is in progress.
+ virtual base::TimeTicks NextRetryAttempt() const = 0;
+};
+
+} // namespace gcm
+
+#endif // GOOGLE_APIS_GCM_ENGINE_CONNECTION_FACTORY_H_
diff --git a/chromium/google_apis/gcm/engine/connection_factory_impl.cc b/chromium/google_apis/gcm/engine/connection_factory_impl.cc
new file mode 100644
index 00000000000..388b9dca5e3
--- /dev/null
+++ b/chromium/google_apis/gcm/engine/connection_factory_impl.cc
@@ -0,0 +1,205 @@
+// Copyright (c) 2013 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "google_apis/gcm/engine/connection_factory_impl.h"
+
+#include "base/message_loop/message_loop.h"
+#include "google_apis/gcm/engine/connection_handler_impl.h"
+#include "google_apis/gcm/protocol/mcs.pb.h"
+#include "net/base/net_errors.h"
+#include "net/http/http_network_session.h"
+#include "net/http/http_request_headers.h"
+#include "net/proxy/proxy_info.h"
+#include "net/socket/client_socket_handle.h"
+#include "net/socket/client_socket_pool_manager.h"
+#include "net/ssl/ssl_config_service.h"
+
+namespace gcm {
+
+namespace {
+
+// The amount of time a Socket read should wait before timing out.
+const int kReadTimeoutMs = 30000; // 30 seconds.
+
+// Backoff policy.
+const net::BackoffEntry::Policy kConnectionBackoffPolicy = {
+ // Number of initial errors (in sequence) to ignore before applying
+ // exponential back-off rules.
+ 0,
+
+ // Initial delay for exponential back-off in ms.
+ 10000, // 10 seconds.
+
+ // Factor by which the waiting time will be multiplied.
+ 2,
+
+ // Fuzzing percentage. ex: 10% will spread requests randomly
+ // between 90%-100% of the calculated time.
+ 0.2, // 20%.
+
+ // Maximum amount of time we are willing to delay our request in ms.
+ 1000 * 3600 * 4, // 4 hours.
+
+ // Time to keep an entry from being discarded even when it
+ // has no significant state, -1 to never discard.
+ -1,
+
+ // Don't use initial delay unless the last request was an error.
+ false,
+};
+
+} // namespace
+
+ConnectionFactoryImpl::ConnectionFactoryImpl(
+ const GURL& mcs_endpoint,
+ scoped_refptr<net::HttpNetworkSession> network_session,
+ net::NetLog* net_log)
+ : mcs_endpoint_(mcs_endpoint),
+ network_session_(network_session),
+ net_log_(net_log),
+ weak_ptr_factory_(this) {
+}
+
+ConnectionFactoryImpl::~ConnectionFactoryImpl() {
+}
+
+void ConnectionFactoryImpl::Initialize(
+ const BuildLoginRequestCallback& request_builder,
+ const ConnectionHandler::ProtoReceivedCallback& read_callback,
+ const ConnectionHandler::ProtoSentCallback& write_callback) {
+ DCHECK(!connection_handler_);
+
+ backoff_entry_ = CreateBackoffEntry(&kConnectionBackoffPolicy);
+ request_builder_ = request_builder;
+
+ net::NetworkChangeNotifier::AddIPAddressObserver(this);
+ net::NetworkChangeNotifier::AddConnectionTypeObserver(this);
+ connection_handler_.reset(
+ new ConnectionHandlerImpl(
+ base::TimeDelta::FromMilliseconds(kReadTimeoutMs),
+ read_callback,
+ write_callback,
+ base::Bind(&ConnectionFactoryImpl::ConnectionHandlerCallback,
+ weak_ptr_factory_.GetWeakPtr())));
+}
+
+ConnectionHandler* ConnectionFactoryImpl::GetConnectionHandler() const {
+ return connection_handler_.get();
+}
+
+void ConnectionFactoryImpl::Connect() {
+ DCHECK(connection_handler_);
+ DCHECK(!IsEndpointReachable());
+
+ if (backoff_entry_->ShouldRejectRequest()) {
+ DVLOG(1) << "Delaying MCS endpoint connection for "
+ << backoff_entry_->GetTimeUntilRelease().InMilliseconds()
+ << " milliseconds.";
+ base::MessageLoop::current()->PostDelayedTask(
+ FROM_HERE,
+ base::Bind(&ConnectionFactoryImpl::Connect,
+ weak_ptr_factory_.GetWeakPtr()),
+ NextRetryAttempt() - base::TimeTicks::Now());
+ return;
+ }
+
+ DVLOG(1) << "Attempting connection to MCS endpoint.";
+ ConnectImpl();
+}
+
+bool ConnectionFactoryImpl::IsEndpointReachable() const {
+ return connection_handler_ && connection_handler_->CanSendMessage();
+}
+
+base::TimeTicks ConnectionFactoryImpl::NextRetryAttempt() const {
+ if (!backoff_entry_)
+ return base::TimeTicks();
+ return backoff_entry_->GetReleaseTime();
+}
+
+void ConnectionFactoryImpl::OnConnectionTypeChanged(
+ net::NetworkChangeNotifier::ConnectionType type) {
+ if (type == net::NetworkChangeNotifier::CONNECTION_NONE)
+ return;
+
+ // TODO(zea): implement different backoff/retry policies based on connection
+ // type.
+ DVLOG(1) << "Connection type changed to " << type << ", resetting backoff.";
+ backoff_entry_->Reset();
+ // Connect(..) should be retrying with backoff already if a connection is
+ // necessary, so no need to call again.
+}
+
+void ConnectionFactoryImpl::OnIPAddressChanged() {
+ DVLOG(1) << "IP Address changed, resetting backoff.";
+ backoff_entry_->Reset();
+ // Connect(..) should be retrying with backoff already if a connection is
+ // necessary, so no need to call again.
+}
+
+void ConnectionFactoryImpl::ConnectImpl() {
+ DCHECK(!IsEndpointReachable());
+
+ // TODO(zea): resolve proxies.
+ net::ProxyInfo proxy_info;
+ proxy_info.UseDirect();
+ net::SSLConfig ssl_config;
+ network_session_->ssl_config_service()->GetSSLConfig(&ssl_config);
+
+ int status = net::InitSocketHandleForTlsConnect(
+ net::HostPortPair::FromURL(mcs_endpoint_),
+ network_session_.get(),
+ proxy_info,
+ ssl_config,
+ ssl_config,
+ net::kPrivacyModeDisabled,
+ net::BoundNetLog::Make(net_log_, net::NetLog::SOURCE_SOCKET),
+ &socket_handle_,
+ base::Bind(&ConnectionFactoryImpl::OnConnectDone,
+ weak_ptr_factory_.GetWeakPtr()));
+ if (status != net::ERR_IO_PENDING)
+ OnConnectDone(status);
+}
+
+void ConnectionFactoryImpl::InitHandler() {
+ // May be null in tests.
+ mcs_proto::LoginRequest login_request;
+ if (!request_builder_.is_null()) {
+ request_builder_.Run(&login_request);
+ DCHECK(login_request.IsInitialized());
+ }
+
+ connection_handler_->Init(login_request, socket_handle_.PassSocket());
+}
+
+scoped_ptr<net::BackoffEntry> ConnectionFactoryImpl::CreateBackoffEntry(
+ const net::BackoffEntry::Policy* const policy) {
+ return scoped_ptr<net::BackoffEntry>(new net::BackoffEntry(policy));
+}
+
+void ConnectionFactoryImpl::OnConnectDone(int result) {
+ if (result != net::OK) {
+ LOG(ERROR) << "Failed to connect to MCS endpoint with error " << result;
+ backoff_entry_->InformOfRequest(false);
+ Connect();
+ return;
+ }
+
+ DVLOG(1) << "MCS endpoint connection success.";
+
+ // Reset the backoff.
+ backoff_entry_->Reset();
+
+ InitHandler();
+}
+
+void ConnectionFactoryImpl::ConnectionHandlerCallback(int result) {
+ // TODO(zea): Consider how to handle errors that may require some sort of
+ // user intervention (login page, etc.).
+ LOG(ERROR) << "Connection reset with error " << result;
+ backoff_entry_->InformOfRequest(false);
+ Connect();
+}
+
+} // namespace gcm
diff --git a/chromium/google_apis/gcm/engine/connection_factory_impl.h b/chromium/google_apis/gcm/engine/connection_factory_impl.h
new file mode 100644
index 00000000000..d807270bfdc
--- /dev/null
+++ b/chromium/google_apis/gcm/engine/connection_factory_impl.h
@@ -0,0 +1,101 @@
+// Copyright (c) 2013 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef GOOGLE_APIS_GCM_ENGINE_CONNECTION_FACTORY_IMPL_H_
+#define GOOGLE_APIS_GCM_ENGINE_CONNECTION_FACTORY_IMPL_H_
+
+#include "google_apis/gcm/engine/connection_factory.h"
+
+#include "base/memory/weak_ptr.h"
+#include "google_apis/gcm/protocol/mcs.pb.h"
+#include "net/base/backoff_entry.h"
+#include "net/base/network_change_notifier.h"
+#include "net/socket/client_socket_handle.h"
+#include "url/gurl.h"
+
+namespace net {
+class HttpNetworkSession;
+class NetLog;
+}
+
+namespace gcm {
+
+class ConnectionHandlerImpl;
+
+class GCM_EXPORT ConnectionFactoryImpl :
+ public ConnectionFactory,
+ public net::NetworkChangeNotifier::ConnectionTypeObserver,
+ public net::NetworkChangeNotifier::IPAddressObserver {
+ public:
+ ConnectionFactoryImpl(
+ const GURL& mcs_endpoint,
+ scoped_refptr<net::HttpNetworkSession> network_session,
+ net::NetLog* net_log);
+ virtual ~ConnectionFactoryImpl();
+
+ // ConnectionFactory implementation.
+ virtual void Initialize(
+ const BuildLoginRequestCallback& request_builder,
+ const ConnectionHandler::ProtoReceivedCallback& read_callback,
+ const ConnectionHandler::ProtoSentCallback& write_callback) OVERRIDE;
+ virtual ConnectionHandler* GetConnectionHandler() const OVERRIDE;
+ virtual void Connect() OVERRIDE;
+ virtual bool IsEndpointReachable() const OVERRIDE;
+ virtual base::TimeTicks NextRetryAttempt() const OVERRIDE;
+
+ // NetworkChangeNotifier observer implementations.
+ virtual void OnConnectionTypeChanged(
+ net::NetworkChangeNotifier::ConnectionType type) OVERRIDE;
+ virtual void OnIPAddressChanged() OVERRIDE;
+
+ protected:
+ // Implementation of Connect(..). If not in backoff, uses |login_request_|
+ // in attempting a connection/handshake. On connection/handshake failure, goes
+ // into backoff.
+ // Virtual for testing.
+ virtual void ConnectImpl();
+
+ // Helper method for initalizing the connection hander.
+ // Virtual for testing.
+ virtual void InitHandler();
+
+ // Helper method for creating a backoff entry.
+ // Virtual for testing.
+ virtual scoped_ptr<net::BackoffEntry> CreateBackoffEntry(
+ const net::BackoffEntry::Policy* const policy);
+
+ // Callback for Socket connection completion.
+ void OnConnectDone(int result);
+
+ private:
+ // ConnectionHandler callback for connection issues.
+ void ConnectionHandlerCallback(int result);
+
+ // The MCS endpoint to make connections to.
+ const GURL mcs_endpoint_;
+
+ // ---- net:: components for establishing connections. ----
+ // Network session for creating new connections.
+ const scoped_refptr<net::HttpNetworkSession> network_session_;
+ // Net log to use in connection attempts.
+ net::NetLog* const net_log_;
+ // The handle to the socket for the current connection, if one exists.
+ net::ClientSocketHandle socket_handle_;
+ // Connection attempt backoff policy.
+ scoped_ptr<net::BackoffEntry> backoff_entry_;
+
+ // The current connection handler, if one exists.
+ scoped_ptr<ConnectionHandlerImpl> connection_handler_;
+
+ // Builder for generating new login requests.
+ BuildLoginRequestCallback request_builder_;
+
+ base::WeakPtrFactory<ConnectionFactoryImpl> weak_ptr_factory_;
+
+ DISALLOW_COPY_AND_ASSIGN(ConnectionFactoryImpl);
+};
+
+} // namespace gcm
+
+#endif // GOOGLE_APIS_GCM_ENGINE_CONNECTION_FACTORY_IMPL_H_
diff --git a/chromium/google_apis/gcm/engine/connection_factory_impl_unittest.cc b/chromium/google_apis/gcm/engine/connection_factory_impl_unittest.cc
new file mode 100644
index 00000000000..1e0ccefef26
--- /dev/null
+++ b/chromium/google_apis/gcm/engine/connection_factory_impl_unittest.cc
@@ -0,0 +1,303 @@
+// Copyright (c) 2013 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "google_apis/gcm/engine/connection_factory_impl.h"
+
+#include <cmath>
+
+#include "base/message_loop/message_loop.h"
+#include "base/run_loop.h"
+#include "base/time/time.h"
+#include "net/base/backoff_entry.h"
+#include "net/http/http_network_session.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+class Policy;
+
+namespace gcm {
+namespace {
+
+const char kMCSEndpoint[] = "http://my.server";
+
+const int kBackoffDelayMs = 1;
+const int kBackoffMultiplier = 2;
+
+// A backoff policy with small enough delays that tests aren't burdened.
+const net::BackoffEntry::Policy kTestBackoffPolicy = {
+ // Number of initial errors (in sequence) to ignore before applying
+ // exponential back-off rules.
+ 0,
+
+ // Initial delay for exponential back-off in ms.
+ kBackoffDelayMs,
+
+ // Factor by which the waiting time will be multiplied.
+ kBackoffMultiplier,
+
+ // Fuzzing percentage. ex: 10% will spread requests randomly
+ // between 90%-100% of the calculated time.
+ 0,
+
+ // Maximum amount of time we are willing to delay our request in ms.
+ 10,
+
+ // Time to keep an entry from being discarded even when it
+ // has no significant state, -1 to never discard.
+ -1,
+
+ // Don't use initial delay unless the last request was an error.
+ false,
+};
+
+// Helper for calculating total expected exponential backoff delay given an
+// arbitrary number of failed attempts. See BackoffEntry::CalculateReleaseTime.
+double CalculateBackoff(int num_attempts) {
+ double delay = kBackoffDelayMs;
+ for (int i = 1; i < num_attempts; ++i) {
+ delay += kBackoffDelayMs * pow(static_cast<double>(kBackoffMultiplier),
+ i - 1);
+ }
+ DVLOG(1) << "Expected backoff " << delay << " milliseconds.";
+ return delay;
+}
+
+// Helper methods that should never actually be called due to real connections
+// being stubbed out.
+void ReadContinuation(
+ scoped_ptr<google::protobuf::MessageLite> message) {
+ ADD_FAILURE();
+}
+
+void WriteContinuation() {
+ ADD_FAILURE();
+}
+
+// A connection factory that stubs out network requests and overrides the
+// backoff policy.
+class TestConnectionFactoryImpl : public ConnectionFactoryImpl {
+ public:
+ TestConnectionFactoryImpl(const base::Closure& finished_callback);
+ virtual ~TestConnectionFactoryImpl();
+
+ // Overridden stubs.
+ virtual void ConnectImpl() OVERRIDE;
+ virtual void InitHandler() OVERRIDE;
+ virtual scoped_ptr<net::BackoffEntry> CreateBackoffEntry(
+ const net::BackoffEntry::Policy* const policy) OVERRIDE;
+
+ // Helpers for verifying connection attempts are made. Connection results
+ // must be consumed.
+ void SetConnectResult(int connect_result);
+ void SetMultipleConnectResults(int connect_result, int num_expected_attempts);
+
+ private:
+ // The result to return on the next connect attempt.
+ int connect_result_;
+ // The number of expected connection attempts;
+ int num_expected_attempts_;
+ // Whether all expected connection attempts have been fulfilled since an
+ // expectation was last set.
+ bool connections_fulfilled_;
+ // Callback to invoke when all connection attempts have been made.
+ base::Closure finished_callback_;
+};
+
+TestConnectionFactoryImpl::TestConnectionFactoryImpl(
+ const base::Closure& finished_callback)
+ : ConnectionFactoryImpl(GURL(kMCSEndpoint), NULL, NULL),
+ connect_result_(net::ERR_UNEXPECTED),
+ num_expected_attempts_(0),
+ connections_fulfilled_(true),
+ finished_callback_(finished_callback) {
+}
+
+TestConnectionFactoryImpl::~TestConnectionFactoryImpl() {
+ EXPECT_EQ(0, num_expected_attempts_);
+}
+
+void TestConnectionFactoryImpl::ConnectImpl() {
+ ASSERT_GT(num_expected_attempts_, 0);
+
+ OnConnectDone(connect_result_);
+ --num_expected_attempts_;
+ if (num_expected_attempts_ == 0) {
+ connect_result_ = net::ERR_UNEXPECTED;
+ connections_fulfilled_ = true;
+ finished_callback_.Run();
+ }
+}
+
+void TestConnectionFactoryImpl::InitHandler() {
+ EXPECT_NE(connect_result_, net::ERR_UNEXPECTED);
+}
+
+scoped_ptr<net::BackoffEntry> TestConnectionFactoryImpl::CreateBackoffEntry(
+ const net::BackoffEntry::Policy* const policy) {
+ return scoped_ptr<net::BackoffEntry>(
+ new net::BackoffEntry(&kTestBackoffPolicy));
+}
+
+void TestConnectionFactoryImpl::SetConnectResult(int connect_result) {
+ DCHECK_NE(connect_result, net::ERR_UNEXPECTED);
+ ASSERT_EQ(0, num_expected_attempts_);
+ connections_fulfilled_ = false;
+ connect_result_ = connect_result;
+ num_expected_attempts_ = 1;
+}
+
+void TestConnectionFactoryImpl::SetMultipleConnectResults(
+ int connect_result,
+ int num_expected_attempts) {
+ DCHECK_NE(connect_result, net::ERR_UNEXPECTED);
+ DCHECK_GT(num_expected_attempts, 0);
+ ASSERT_EQ(0, num_expected_attempts_);
+ connections_fulfilled_ = false;
+ connect_result_ = connect_result;
+ num_expected_attempts_ = num_expected_attempts;
+}
+
+class ConnectionFactoryImplTest : public testing::Test {
+ public:
+ ConnectionFactoryImplTest();
+ virtual ~ConnectionFactoryImplTest();
+
+ TestConnectionFactoryImpl* factory() { return &factory_; }
+
+ void WaitForConnections();
+
+ private:
+ void ConnectionsComplete();
+
+ TestConnectionFactoryImpl factory_;
+ base::MessageLoop message_loop_;
+ scoped_ptr<base::RunLoop> run_loop_;
+};
+
+ConnectionFactoryImplTest::ConnectionFactoryImplTest()
+ : factory_(base::Bind(&ConnectionFactoryImplTest::ConnectionsComplete,
+ base::Unretained(this))),
+ run_loop_(new base::RunLoop()) {}
+ConnectionFactoryImplTest::~ConnectionFactoryImplTest() {}
+
+void ConnectionFactoryImplTest::WaitForConnections() {
+ run_loop_->Run();
+ run_loop_.reset(new base::RunLoop());
+}
+
+void ConnectionFactoryImplTest::ConnectionsComplete() {
+ if (!run_loop_)
+ return;
+ run_loop_->Quit();
+}
+
+// Verify building a connection handler works.
+TEST_F(ConnectionFactoryImplTest, Initialize) {
+ EXPECT_FALSE(factory()->IsEndpointReachable());
+ factory()->Initialize(
+ ConnectionFactory::BuildLoginRequestCallback(),
+ base::Bind(&ReadContinuation),
+ base::Bind(&WriteContinuation));
+ ConnectionHandler* handler = factory()->GetConnectionHandler();
+ ASSERT_TRUE(handler);
+ EXPECT_FALSE(factory()->IsEndpointReachable());
+}
+
+// An initial successful connection should not result in backoff.
+TEST_F(ConnectionFactoryImplTest, ConnectSuccess) {
+ factory()->Initialize(
+ ConnectionFactory::BuildLoginRequestCallback(),
+ ConnectionHandler::ProtoReceivedCallback(),
+ ConnectionHandler::ProtoSentCallback());
+ factory()->SetConnectResult(net::OK);
+ factory()->Connect();
+ EXPECT_TRUE(factory()->NextRetryAttempt().is_null());
+}
+
+// A connection failure should result in backoff.
+TEST_F(ConnectionFactoryImplTest, ConnectFail) {
+ factory()->Initialize(
+ ConnectionFactory::BuildLoginRequestCallback(),
+ ConnectionHandler::ProtoReceivedCallback(),
+ ConnectionHandler::ProtoSentCallback());
+ factory()->SetConnectResult(net::ERR_CONNECTION_FAILED);
+ factory()->Connect();
+ EXPECT_FALSE(factory()->NextRetryAttempt().is_null());
+}
+
+// A connection success after a failure should reset backoff.
+TEST_F(ConnectionFactoryImplTest, FailThenSucceed) {
+ factory()->Initialize(
+ ConnectionFactory::BuildLoginRequestCallback(),
+ ConnectionHandler::ProtoReceivedCallback(),
+ ConnectionHandler::ProtoSentCallback());
+ factory()->SetConnectResult(net::ERR_CONNECTION_FAILED);
+ base::TimeTicks connect_time = base::TimeTicks::Now();
+ factory()->Connect();
+ WaitForConnections();
+ base::TimeTicks retry_time = factory()->NextRetryAttempt();
+ EXPECT_FALSE(retry_time.is_null());
+ EXPECT_GE((retry_time - connect_time).InMilliseconds(), CalculateBackoff(1));
+ factory()->SetConnectResult(net::OK);
+ WaitForConnections();
+ EXPECT_TRUE(factory()->NextRetryAttempt().is_null());
+}
+
+// Multiple connection failures should retry with an exponentially increasing
+// backoff, then reset on success.
+TEST_F(ConnectionFactoryImplTest, MultipleFailuresThenSucceed) {
+ factory()->Initialize(
+ ConnectionFactory::BuildLoginRequestCallback(),
+ ConnectionHandler::ProtoReceivedCallback(),
+ ConnectionHandler::ProtoSentCallback());
+
+ const int kNumAttempts = 5;
+ factory()->SetMultipleConnectResults(net::ERR_CONNECTION_FAILED,
+ kNumAttempts);
+
+ base::TimeTicks connect_time = base::TimeTicks::Now();
+ factory()->Connect();
+ WaitForConnections();
+ base::TimeTicks retry_time = factory()->NextRetryAttempt();
+ EXPECT_FALSE(retry_time.is_null());
+ EXPECT_GE((retry_time - connect_time).InMilliseconds(),
+ CalculateBackoff(kNumAttempts));
+
+ factory()->SetConnectResult(net::OK);
+ WaitForConnections();
+ EXPECT_TRUE(factory()->NextRetryAttempt().is_null());
+}
+
+// IP events should reset backoff.
+TEST_F(ConnectionFactoryImplTest, FailThenIPEvent) {
+ factory()->Initialize(
+ ConnectionFactory::BuildLoginRequestCallback(),
+ ConnectionHandler::ProtoReceivedCallback(),
+ ConnectionHandler::ProtoSentCallback());
+ factory()->SetConnectResult(net::ERR_CONNECTION_FAILED);
+ factory()->Connect();
+ WaitForConnections();
+ EXPECT_FALSE(factory()->NextRetryAttempt().is_null());
+
+ factory()->OnIPAddressChanged();
+ EXPECT_TRUE(factory()->NextRetryAttempt().is_null());
+}
+
+// Connection type events should reset backoff.
+TEST_F(ConnectionFactoryImplTest, FailThenConnectionTypeEvent) {
+ factory()->Initialize(
+ ConnectionFactory::BuildLoginRequestCallback(),
+ ConnectionHandler::ProtoReceivedCallback(),
+ ConnectionHandler::ProtoSentCallback());
+ factory()->SetConnectResult(net::ERR_CONNECTION_FAILED);
+ factory()->Connect();
+ WaitForConnections();
+ EXPECT_FALSE(factory()->NextRetryAttempt().is_null());
+
+ factory()->OnConnectionTypeChanged(
+ net::NetworkChangeNotifier::CONNECTION_WIFI);
+ EXPECT_TRUE(factory()->NextRetryAttempt().is_null());
+}
+
+} // namespace
+} // namespace gcm
diff --git a/chromium/google_apis/gcm/engine/connection_handler.cc b/chromium/google_apis/gcm/engine/connection_handler.cc
new file mode 100644
index 00000000000..bc9b6585979
--- /dev/null
+++ b/chromium/google_apis/gcm/engine/connection_handler.cc
@@ -0,0 +1,15 @@
+// Copyright 2013 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "google_apis/gcm/engine/connection_handler.h"
+
+namespace gcm {
+
+ConnectionHandler::ConnectionHandler() {
+}
+
+ConnectionHandler::~ConnectionHandler() {
+}
+
+} // namespace gcm
diff --git a/chromium/google_apis/gcm/engine/connection_handler.h b/chromium/google_apis/gcm/engine/connection_handler.h
new file mode 100644
index 00000000000..5b9ea715c78
--- /dev/null
+++ b/chromium/google_apis/gcm/engine/connection_handler.h
@@ -0,0 +1,63 @@
+// Copyright 2013 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef GOOGLE_APIS_GCM_ENGINE_CONNECTION_HANDLER_H_
+#define GOOGLE_APIS_GCM_ENGINE_CONNECTION_HANDLER_H_
+
+#include "base/callback.h"
+#include "google_apis/gcm/base/gcm_export.h"
+
+namespace net{
+class StreamSocket;
+} // namespace net
+
+namespace google {
+namespace protobuf {
+class MessageLite;
+} // namespace protobuf
+} // namepsace google
+
+namespace mcs_proto {
+class LoginRequest;
+}
+
+namespace gcm {
+
+class SocketInputStream;
+class SocketOutputStream;
+
+// Handles performing the protocol handshake and sending/receiving protobuf
+// messages. Note that no retrying or queueing is enforced at this layer.
+// Once a connection error is encountered, the ConnectionHandler will disconnect
+// the socket and must be reinitialized with a new StreamSocket before
+// messages can be sent/received again.
+class GCM_EXPORT ConnectionHandler {
+ public:
+ typedef base::Callback<void(scoped_ptr<google::protobuf::MessageLite>)>
+ ProtoReceivedCallback;
+ typedef base::Closure ProtoSentCallback;
+ typedef base::Callback<void(int)> ConnectionChangedCallback;
+
+ ConnectionHandler();
+ virtual ~ConnectionHandler();
+
+ // Starts a new MCS connection handshake (using |login_request|) and, upon
+ // success, begins listening for incoming/outgoing messages.
+ //
+ // Note: It is correct and expected to call Init more than once, as connection
+ // issues are encountered and new connections must be made.
+ virtual void Init(const mcs_proto::LoginRequest& login_request,
+ scoped_ptr<net::StreamSocket> socket) = 0;
+
+ // Checks that a handshake has been completed and a message is not already
+ // in flight.
+ virtual bool CanSendMessage() const = 0;
+
+ // Send an MCS protobuf message. CanSendMessage() must be true.
+ virtual void SendMessage(const google::protobuf::MessageLite& message) = 0;
+};
+
+} // namespace gcm
+
+#endif // GOOGLE_APIS_GCM_ENGINE_CONNECTION_HANDLER_H_
diff --git a/chromium/google_apis/gcm/engine/connection_handler_impl.cc b/chromium/google_apis/gcm/engine/connection_handler_impl.cc
new file mode 100644
index 00000000000..aff0dfd3651
--- /dev/null
+++ b/chromium/google_apis/gcm/engine/connection_handler_impl.cc
@@ -0,0 +1,404 @@
+// Copyright 2013 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "google_apis/gcm/engine/connection_handler_impl.h"
+
+#include "base/message_loop/message_loop.h"
+#include "google/protobuf/io/coded_stream.h"
+#include "google_apis/gcm/base/mcs_util.h"
+#include "google_apis/gcm/base/socket_stream.h"
+#include "google_apis/gcm/protocol/mcs.pb.h"
+#include "net/base/net_errors.h"
+#include "net/socket/stream_socket.h"
+
+using namespace google::protobuf::io;
+
+namespace gcm {
+
+namespace {
+
+// # of bytes a MCS version packet consumes.
+const int kVersionPacketLen = 1;
+// # of bytes a tag packet consumes.
+const int kTagPacketLen = 1;
+// Max # of bytes a length packet consumes.
+const int kSizePacketLenMin = 1;
+const int kSizePacketLenMax = 2;
+
+// The current MCS protocol version.
+// TODO(zea): bump to 41 once the server supports it.
+const int kMCSVersion = 38;
+
+} // namespace
+
+ConnectionHandlerImpl::ConnectionHandlerImpl(
+ base::TimeDelta read_timeout,
+ const ProtoReceivedCallback& read_callback,
+ const ProtoSentCallback& write_callback,
+ const ConnectionChangedCallback& connection_callback)
+ : read_timeout_(read_timeout),
+ handshake_complete_(false),
+ message_tag_(0),
+ message_size_(0),
+ read_callback_(read_callback),
+ write_callback_(write_callback),
+ connection_callback_(connection_callback),
+ weak_ptr_factory_(this) {
+}
+
+ConnectionHandlerImpl::~ConnectionHandlerImpl() {
+}
+
+void ConnectionHandlerImpl::Init(
+ const mcs_proto::LoginRequest& login_request,
+ scoped_ptr<net::StreamSocket> socket) {
+ DCHECK(!read_callback_.is_null());
+ DCHECK(!write_callback_.is_null());
+ DCHECK(!connection_callback_.is_null());
+
+ // Invalidate any previously outstanding reads.
+ weak_ptr_factory_.InvalidateWeakPtrs();
+
+ handshake_complete_ = false;
+ message_tag_ = 0;
+ message_size_ = 0;
+ socket_ = socket.Pass();
+ input_stream_.reset(new SocketInputStream(socket_.get()));
+ output_stream_.reset(new SocketOutputStream(socket_.get()));
+
+ Login(login_request);
+}
+
+bool ConnectionHandlerImpl::CanSendMessage() const {
+ return handshake_complete_ && output_stream_.get() &&
+ output_stream_->GetState() == SocketOutputStream::EMPTY;
+}
+
+void ConnectionHandlerImpl::SendMessage(
+ const google::protobuf::MessageLite& message) {
+ DCHECK_EQ(output_stream_->GetState(), SocketOutputStream::EMPTY);
+ DCHECK(handshake_complete_);
+
+ {
+ CodedOutputStream coded_output_stream(output_stream_.get());
+ DVLOG(1) << "Writing proto of size " << message.ByteSize();
+ int tag = GetMCSProtoTag(message);
+ DCHECK_NE(tag, -1);
+ coded_output_stream.WriteRaw(&tag, 1);
+ coded_output_stream.WriteVarint32(message.ByteSize());
+ message.SerializeToCodedStream(&coded_output_stream);
+ }
+
+ if (output_stream_->Flush(
+ base::Bind(&ConnectionHandlerImpl::OnMessageSent,
+ weak_ptr_factory_.GetWeakPtr())) != net::ERR_IO_PENDING) {
+ OnMessageSent();
+ }
+}
+
+void ConnectionHandlerImpl::Login(
+ const google::protobuf::MessageLite& login_request) {
+ DCHECK_EQ(output_stream_->GetState(), SocketOutputStream::EMPTY);
+
+ const char version_byte[1] = {kMCSVersion};
+ const char login_request_tag[1] = {kLoginRequestTag};
+ {
+ CodedOutputStream coded_output_stream(output_stream_.get());
+ coded_output_stream.WriteRaw(version_byte, 1);
+ coded_output_stream.WriteRaw(login_request_tag, 1);
+ coded_output_stream.WriteVarint32(login_request.ByteSize());
+ login_request.SerializeToCodedStream(&coded_output_stream);
+ }
+
+ if (output_stream_->Flush(
+ base::Bind(&ConnectionHandlerImpl::OnMessageSent,
+ weak_ptr_factory_.GetWeakPtr())) != net::ERR_IO_PENDING) {
+ base::MessageLoop::current()->PostTask(
+ FROM_HERE,
+ base::Bind(&ConnectionHandlerImpl::OnMessageSent,
+ weak_ptr_factory_.GetWeakPtr()));
+ }
+
+ read_timeout_timer_.Start(FROM_HERE,
+ read_timeout_,
+ base::Bind(&ConnectionHandlerImpl::OnTimeout,
+ weak_ptr_factory_.GetWeakPtr()));
+ WaitForData(MCS_VERSION_TAG_AND_SIZE);
+}
+
+void ConnectionHandlerImpl::OnMessageSent() {
+ if (!output_stream_.get()) {
+ // The connection has already been closed. Just return.
+ DCHECK(!input_stream_.get());
+ DCHECK(!read_timeout_timer_.IsRunning());
+ return;
+ }
+
+ if (output_stream_->GetState() != SocketOutputStream::EMPTY) {
+ int last_error = output_stream_->last_error();
+ CloseConnection();
+ // If the socket stream had an error, plumb it up, else plumb up FAILED.
+ if (last_error == net::OK)
+ last_error = net::ERR_FAILED;
+ connection_callback_.Run(last_error);
+ return;
+ }
+
+ write_callback_.Run();
+}
+
+void ConnectionHandlerImpl::GetNextMessage() {
+ DCHECK(SocketInputStream::EMPTY == input_stream_->GetState() ||
+ SocketInputStream::READY == input_stream_->GetState());
+ message_tag_ = 0;
+ message_size_ = 0;
+
+ WaitForData(MCS_TAG_AND_SIZE);
+}
+
+void ConnectionHandlerImpl::WaitForData(ProcessingState state) {
+ DVLOG(1) << "Waiting for MCS data: state == " << state;
+
+ if (!input_stream_) {
+ // The connection has already been closed. Just return.
+ DCHECK(!output_stream_.get());
+ DCHECK(!read_timeout_timer_.IsRunning());
+ return;
+ }
+
+ if (input_stream_->GetState() != SocketInputStream::EMPTY &&
+ input_stream_->GetState() != SocketInputStream::READY) {
+ // An error occurred.
+ int last_error = output_stream_->last_error();
+ CloseConnection();
+ // If the socket stream had an error, plumb it up, else plumb up FAILED.
+ if (last_error == net::OK)
+ last_error = net::ERR_FAILED;
+ connection_callback_.Run(last_error);
+ return;
+ }
+
+ // Used to determine whether a Socket::Read is necessary.
+ int min_bytes_needed = 0;
+ // Used to limit the size of the Socket::Read.
+ int max_bytes_needed = 0;
+
+ switch(state) {
+ case MCS_VERSION_TAG_AND_SIZE:
+ min_bytes_needed = kVersionPacketLen + kTagPacketLen + kSizePacketLenMin;
+ max_bytes_needed = kVersionPacketLen + kTagPacketLen + kSizePacketLenMax;
+ break;
+ case MCS_TAG_AND_SIZE:
+ min_bytes_needed = kTagPacketLen + kSizePacketLenMin;
+ max_bytes_needed = kTagPacketLen + kSizePacketLenMax;
+ break;
+ case MCS_FULL_SIZE:
+ // If in this state, the minimum size packet length must already have been
+ // insufficient, so set both to the max length.
+ min_bytes_needed = kSizePacketLenMax;
+ max_bytes_needed = kSizePacketLenMax;
+ break;
+ case MCS_PROTO_BYTES:
+ read_timeout_timer_.Reset();
+ // No variability in the message size, set both to the same.
+ min_bytes_needed = message_size_;
+ max_bytes_needed = message_size_;
+ break;
+ default:
+ NOTREACHED();
+ }
+ DCHECK_GE(max_bytes_needed, min_bytes_needed);
+
+ int byte_count = input_stream_->UnreadByteCount();
+ if (min_bytes_needed - byte_count > 0 &&
+ input_stream_->Refresh(
+ base::Bind(&ConnectionHandlerImpl::WaitForData,
+ weak_ptr_factory_.GetWeakPtr(),
+ state),
+ max_bytes_needed - byte_count) == net::ERR_IO_PENDING) {
+ return;
+ }
+
+ // Check for refresh errors.
+ if (input_stream_->GetState() != SocketInputStream::READY) {
+ // An error occurred.
+ int last_error = output_stream_->last_error();
+ CloseConnection();
+ // If the socket stream had an error, plumb it up, else plumb up FAILED.
+ if (last_error == net::OK)
+ last_error = net::ERR_FAILED;
+ connection_callback_.Run(last_error);
+ return;
+ }
+
+ // Received enough bytes, process them.
+ DVLOG(1) << "Processing MCS data: state == " << state;
+ switch(state) {
+ case MCS_VERSION_TAG_AND_SIZE:
+ OnGotVersion();
+ break;
+ case MCS_TAG_AND_SIZE:
+ OnGotMessageTag();
+ break;
+ case MCS_FULL_SIZE:
+ OnGotMessageSize();
+ break;
+ case MCS_PROTO_BYTES:
+ OnGotMessageBytes();
+ break;
+ default:
+ NOTREACHED();
+ }
+}
+
+void ConnectionHandlerImpl::OnGotVersion() {
+ uint8 version = 0;
+ {
+ CodedInputStream coded_input_stream(input_stream_.get());
+ coded_input_stream.ReadRaw(&version, 1);
+ }
+ if (version < kMCSVersion) {
+ LOG(ERROR) << "Invalid GCM version response: " << static_cast<int>(version);
+ connection_callback_.Run(net::ERR_FAILED);
+ return;
+ }
+
+ input_stream_->RebuildBuffer();
+
+ // Process the LoginResponse message tag.
+ OnGotMessageTag();
+}
+
+void ConnectionHandlerImpl::OnGotMessageTag() {
+ if (input_stream_->GetState() != SocketInputStream::READY) {
+ LOG(ERROR) << "Failed to receive protobuf tag.";
+ read_callback_.Run(scoped_ptr<google::protobuf::MessageLite>());
+ return;
+ }
+
+ {
+ CodedInputStream coded_input_stream(input_stream_.get());
+ coded_input_stream.ReadRaw(&message_tag_, 1);
+ }
+
+ DVLOG(1) << "Received proto of type "
+ << static_cast<unsigned int>(message_tag_);
+
+ if (!read_timeout_timer_.IsRunning()) {
+ read_timeout_timer_.Start(FROM_HERE,
+ read_timeout_,
+ base::Bind(&ConnectionHandlerImpl::OnTimeout,
+ weak_ptr_factory_.GetWeakPtr()));
+ }
+ OnGotMessageSize();
+}
+
+void ConnectionHandlerImpl::OnGotMessageSize() {
+ if (input_stream_->GetState() != SocketInputStream::READY) {
+ LOG(ERROR) << "Failed to receive message size.";
+ read_callback_.Run(scoped_ptr<google::protobuf::MessageLite>());
+ return;
+ }
+
+ bool need_another_byte = false;
+ int prev_byte_count = input_stream_->ByteCount();
+ {
+ CodedInputStream coded_input_stream(input_stream_.get());
+ if (!coded_input_stream.ReadVarint32(&message_size_))
+ need_another_byte = true;
+ }
+
+ if (need_another_byte) {
+ DVLOG(1) << "Expecting another message size byte.";
+ if (prev_byte_count >= kSizePacketLenMax) {
+ // Already had enough bytes, something else went wrong.
+ LOG(ERROR) << "Failed to process message size.";
+ read_callback_.Run(scoped_ptr<google::protobuf::MessageLite>());
+ return;
+ }
+ // Back up by the amount read (should always be 1 byte).
+ int bytes_read = prev_byte_count - input_stream_->ByteCount();
+ DCHECK_EQ(bytes_read, 1);
+ input_stream_->BackUp(bytes_read);
+ WaitForData(MCS_FULL_SIZE);
+ return;
+ }
+
+ DVLOG(1) << "Proto size: " << message_size_;
+
+ if (message_size_ > 0)
+ WaitForData(MCS_PROTO_BYTES);
+ else
+ OnGotMessageBytes();
+}
+
+void ConnectionHandlerImpl::OnGotMessageBytes() {
+ read_timeout_timer_.Stop();
+ scoped_ptr<google::protobuf::MessageLite> protobuf(
+ BuildProtobufFromTag(message_tag_));
+ // Messages with no content are valid; just use the default protobuf for
+ // that tag.
+ if (protobuf.get() && message_size_ == 0) {
+ base::MessageLoop::current()->PostTask(
+ FROM_HERE,
+ base::Bind(&ConnectionHandlerImpl::GetNextMessage,
+ weak_ptr_factory_.GetWeakPtr()));
+ read_callback_.Run(protobuf.Pass());
+ return;
+ }
+
+ if (!protobuf.get() ||
+ input_stream_->GetState() != SocketInputStream::READY) {
+ LOG(ERROR) << "Failed to extract protobuf bytes of type "
+ << static_cast<unsigned int>(message_tag_);
+ protobuf.reset(); // Return a null pointer to denote an error.
+ read_callback_.Run(protobuf.Pass());
+ return;
+ }
+
+ {
+ CodedInputStream coded_input_stream(input_stream_.get());
+ if (!protobuf->ParsePartialFromCodedStream(&coded_input_stream)) {
+ NOTREACHED() << "Unable to parse GCM message of type "
+ << static_cast<unsigned int>(message_tag_);
+ protobuf.reset(); // Return a null pointer to denote an error.
+ read_callback_.Run(protobuf.Pass());
+ return;
+ }
+ }
+
+ input_stream_->RebuildBuffer();
+ base::MessageLoop::current()->PostTask(
+ FROM_HERE,
+ base::Bind(&ConnectionHandlerImpl::GetNextMessage,
+ weak_ptr_factory_.GetWeakPtr()));
+ if (message_tag_ == kLoginResponseTag) {
+ if (handshake_complete_) {
+ LOG(ERROR) << "Unexpected login response.";
+ } else {
+ handshake_complete_ = true;
+ DVLOG(1) << "GCM Handshake complete.";
+ }
+ }
+ read_callback_.Run(protobuf.Pass());
+}
+
+void ConnectionHandlerImpl::OnTimeout() {
+ LOG(ERROR) << "Timed out waiting for GCM Protocol buffer.";
+ CloseConnection();
+ connection_callback_.Run(net::ERR_TIMED_OUT);
+}
+
+void ConnectionHandlerImpl::CloseConnection() {
+ DVLOG(1) << "Closing connection.";
+ read_callback_.Reset();
+ write_callback_.Reset();
+ read_timeout_timer_.Stop();
+ socket_->Disconnect();
+ input_stream_.reset();
+ output_stream_.reset();
+ weak_ptr_factory_.InvalidateWeakPtrs();
+}
+
+} // namespace gcm
diff --git a/chromium/google_apis/gcm/engine/connection_handler_impl.h b/chromium/google_apis/gcm/engine/connection_handler_impl.h
new file mode 100644
index 00000000000..110cdcdddda
--- /dev/null
+++ b/chromium/google_apis/gcm/engine/connection_handler_impl.h
@@ -0,0 +1,122 @@
+// Copyright 2013 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef GOOGLE_APIS_GCM_ENGINE_CONNECTION_HANDLER_IMPL_H_
+#define GOOGLE_APIS_GCM_ENGINE_CONNECTION_HANDLER_IMPL_H_
+
+#include "base/memory/weak_ptr.h"
+#include "base/time/time.h"
+#include "base/timer/timer.h"
+#include "google_apis/gcm/engine/connection_handler.h"
+
+namespace mcs_proto {
+class LoginRequest;
+} // namespace mcs_proto
+
+namespace gcm {
+
+class GCM_EXPORT ConnectionHandlerImpl : public ConnectionHandler {
+ public:
+ // |read_callback| will be invoked with the contents of any received protobuf
+ // message.
+ // |write_callback| will be invoked anytime a message has been successfully
+ // sent. Note: this just means the data was sent to the wire, not that the
+ // other end received it.
+ // |connection_callback| will be invoked with any fatal read/write errors
+ // encountered.
+ ConnectionHandlerImpl(
+ base::TimeDelta read_timeout,
+ const ProtoReceivedCallback& read_callback,
+ const ProtoSentCallback& write_callback,
+ const ConnectionChangedCallback& connection_callback);
+ virtual ~ConnectionHandlerImpl();
+
+ // ConnectionHandler implementation.
+ virtual void Init(const mcs_proto::LoginRequest& login_request,
+ scoped_ptr<net::StreamSocket> socket) OVERRIDE;
+ virtual bool CanSendMessage() const OVERRIDE;
+ virtual void SendMessage(const google::protobuf::MessageLite& message)
+ OVERRIDE;
+
+ private:
+ // State machine for handling incoming data. See WaitForData(..) for usage.
+ enum ProcessingState {
+ // Processing the version, tag, and size packets (assuming minimum length
+ // size packet). Only used during the login handshake.
+ MCS_VERSION_TAG_AND_SIZE = 0,
+ // Processing the tag and size packets (assuming minimum length size
+ // packet). Used for normal messages.
+ MCS_TAG_AND_SIZE,
+ // Processing a maximum length size packet (for messages with length > 128).
+ // Used when a normal size packet was not sufficient to read the message
+ // size.
+ MCS_FULL_SIZE,
+ // Processing the protocol buffer bytes (for those messages with non-zero
+ // sizes).
+ MCS_PROTO_BYTES
+ };
+
+ // Sends the protocol version and login request. First step in the MCS
+ // connection handshake.
+ void Login(const google::protobuf::MessageLite& login_request);
+
+ // SendMessage continuation. Invoked when Socket::Write completes.
+ void OnMessageSent();
+
+ // Starts the message processing process, which is comprised of the tag,
+ // message size, and bytes packet types.
+ void GetNextMessage();
+
+ // Performs any necessary SocketInputStream refreshing until the data
+ // associated with |packet_type| is fully ready, then calls the appropriate
+ // OnGot* message to process the packet data. If the read times out,
+ // will close the stream and invoke the connection callback.
+ void WaitForData(ProcessingState state);
+
+ // Incoming data helper methods.
+ void OnGotVersion();
+ void OnGotMessageTag();
+ void OnGotMessageSize();
+ void OnGotMessageBytes();
+
+ // Timeout handler.
+ void OnTimeout();
+
+ // Closes the current connection.
+ void CloseConnection();
+
+ // Timeout policy: the timeout is only enforced while waiting on the
+ // handshake (version and/or LoginResponse) or once at least a tag packet has
+ // been received. It is reset every time new data is received, and is
+ // only stopped when a full message is processed.
+ // TODO(zea): consider enforcing a separate timeout when waiting for
+ // a message to send.
+ const base::TimeDelta read_timeout_;
+ base::OneShotTimer<ConnectionHandlerImpl> read_timeout_timer_;
+
+ // This connection's socket and the input/output streams attached to it.
+ scoped_ptr<net::StreamSocket> socket_;
+ scoped_ptr<SocketInputStream> input_stream_;
+ scoped_ptr<SocketOutputStream> output_stream_;
+
+ // Whether the MCS login handshake has successfully completed. See Init(..)
+ // description for more info on what the handshake involves.
+ bool handshake_complete_;
+
+ // State for the message currently being processed, if there is one.
+ uint8 message_tag_;
+ uint32 message_size_;
+
+ ProtoReceivedCallback read_callback_;
+ ProtoSentCallback write_callback_;
+ ConnectionChangedCallback connection_callback_;
+
+ base::WeakPtrFactory<ConnectionHandlerImpl> weak_ptr_factory_;
+
+ DISALLOW_COPY_AND_ASSIGN(ConnectionHandlerImpl);
+};
+
+} // namespace gcm
+
+#endif // GOOGLE_APIS_GCM_ENGINE_CONNECTION_HANDLER_IMPL_H_
diff --git a/chromium/google_apis/gcm/engine/connection_handler_impl_unittest.cc b/chromium/google_apis/gcm/engine/connection_handler_impl_unittest.cc
new file mode 100644
index 00000000000..0cdcdc621ff
--- /dev/null
+++ b/chromium/google_apis/gcm/engine/connection_handler_impl_unittest.cc
@@ -0,0 +1,628 @@
+// Copyright 2013 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "google_apis/gcm/engine/connection_handler_impl.h"
+
+#include "base/bind.h"
+#include "base/memory/scoped_ptr.h"
+#include "base/run_loop.h"
+#include "base/strings/string_number_conversions.h"
+#include "base/test/test_timeouts.h"
+#include "google/protobuf/io/coded_stream.h"
+#include "google/protobuf/io/zero_copy_stream_impl_lite.h"
+#include "google_apis/gcm/base/mcs_util.h"
+#include "google_apis/gcm/base/socket_stream.h"
+#include "google_apis/gcm/protocol/mcs.pb.h"
+#include "net/socket/socket_test_util.h"
+#include "net/socket/stream_socket.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+namespace gcm {
+namespace {
+
+typedef scoped_ptr<google::protobuf::MessageLite> ScopedMessage;
+typedef std::vector<net::MockRead> ReadList;
+typedef std::vector<net::MockWrite> WriteList;
+
+const uint64 kAuthId = 54321;
+const uint64 kAuthToken = 12345;
+const char kMCSVersion = 38; // The protocol version.
+const int kMCSPort = 5228; // The server port.
+const char kDataMsgFrom[] = "data_from";
+const char kDataMsgCategory[] = "data_category";
+const char kDataMsgFrom2[] = "data_from2";
+const char kDataMsgCategory2[] = "data_category2";
+const char kDataMsgFromLong[] =
+ "this is a long from that will result in a message > 128 bytes";
+const char kDataMsgCategoryLong[] =
+ "this is a long category that will result in a message > 128 bytes";
+const char kDataMsgFromLong2[] =
+ "this is a second long from that will result in a message > 128 bytes";
+const char kDataMsgCategoryLong2[] =
+ "this is a second long category that will result in a message > 128 bytes";
+
+// ---- Helpers for building messages. ----
+
+// Encode a protobuf packet with protobuf type |tag| and serialized protobuf
+// bytes |proto| into the MCS message form (tag + varint size + bytes).
+std::string EncodePacket(uint8 tag, const std::string& proto) {
+ std::string result;
+ google::protobuf::io::StringOutputStream string_output_stream(&result);
+ google::protobuf::io::CodedOutputStream coded_output_stream(
+ &string_output_stream);
+ const unsigned char tag_byte[1] = {tag};
+ coded_output_stream.WriteRaw(tag_byte, 1);
+ coded_output_stream.WriteVarint32(proto.size());
+ coded_output_stream.WriteRaw(proto.c_str(), proto.size());
+ return result;
+}
+
+// Encode a handshake request into the MCS message form.
+std::string EncodeHandshakeRequest() {
+ std::string result;
+ const char version_byte[1] = {kMCSVersion};
+ result.append(version_byte, 1);
+ ScopedMessage login_request(BuildLoginRequest(kAuthId, kAuthToken));
+ result.append(EncodePacket(kLoginRequestTag,
+ login_request->SerializeAsString()));
+ return result;
+}
+
+// Build a serialized login response protobuf.
+std::string BuildLoginResponse() {
+ std::string result;
+ mcs_proto::LoginResponse login_response;
+ login_response.set_id("id");
+ result.append(login_response.SerializeAsString());
+ return result;
+}
+
+// Encoode a handshake response into the MCS message form.
+std::string EncodeHandshakeResponse() {
+ std::string result;
+ const char version_byte[1] = {kMCSVersion};
+ result.append(version_byte, 1);
+ result.append(EncodePacket(kLoginResponseTag, BuildLoginResponse()));
+ return result;
+}
+
+// Build a serialized data message stanza protobuf.
+std::string BuildDataMessage(const std::string& from,
+ const std::string& category) {
+ std::string result;
+ mcs_proto::DataMessageStanza data_message;
+ data_message.set_from(from);
+ data_message.set_category(category);
+ return data_message.SerializeAsString();
+}
+
+class GCMConnectionHandlerImplTest : public testing::Test {
+ public:
+ GCMConnectionHandlerImplTest();
+ virtual ~GCMConnectionHandlerImplTest();
+
+ net::StreamSocket* BuildSocket(const ReadList& read_list,
+ const WriteList& write_list);
+
+ // Pump |message_loop_|, resetting |run_loop_| after completion.
+ void PumpLoop();
+
+ ConnectionHandlerImpl* connection_handler() {
+ return connection_handler_.get();
+ }
+ base::MessageLoop* message_loop() { return &message_loop_; };
+ net::DelayedSocketData* data_provider() { return data_provider_.get(); }
+ int last_error() const { return last_error_; }
+
+ // Initialize the connection handler, setting |dst_proto| as the destination
+ // for any received messages.
+ void Connect(ScopedMessage* dst_proto);
+
+ // Runs the message loop until a message is received.
+ void WaitForMessage();
+
+ private:
+ void ReadContinuation(ScopedMessage* dst_proto, ScopedMessage new_proto);
+ void WriteContinuation();
+ void ConnectionContinuation(int error);
+
+ // SocketStreams and their data provider.
+ ReadList mock_reads_;
+ WriteList mock_writes_;
+ scoped_ptr<net::DelayedSocketData> data_provider_;
+ scoped_ptr<SocketInputStream> socket_input_stream_;
+ scoped_ptr<SocketOutputStream> socket_output_stream_;
+
+ // The connection handler being tested.
+ scoped_ptr<ConnectionHandlerImpl> connection_handler_;
+
+ // The last connection error received.
+ int last_error_;
+
+ // net:: components.
+ scoped_ptr<net::StreamSocket> socket_;
+ net::MockClientSocketFactory socket_factory_;
+ net::AddressList address_list_;
+
+ base::MessageLoopForIO message_loop_;
+ scoped_ptr<base::RunLoop> run_loop_;
+};
+
+GCMConnectionHandlerImplTest::GCMConnectionHandlerImplTest()
+ : last_error_(0) {
+ net::IPAddressNumber ip_number;
+ net::ParseIPLiteralToNumber("127.0.0.1", &ip_number);
+ address_list_ = net::AddressList::CreateFromIPAddress(ip_number, kMCSPort);
+}
+
+GCMConnectionHandlerImplTest::~GCMConnectionHandlerImplTest() {
+}
+
+net::StreamSocket* GCMConnectionHandlerImplTest::BuildSocket(
+ const ReadList& read_list,
+ const WriteList& write_list) {
+ mock_reads_ = read_list;
+ mock_writes_ = write_list;
+ data_provider_.reset(
+ new net::DelayedSocketData(0,
+ &(mock_reads_[0]), mock_reads_.size(),
+ &(mock_writes_[0]), mock_writes_.size()));
+ socket_factory_.AddSocketDataProvider(data_provider_.get());
+
+ socket_ = socket_factory_.CreateTransportClientSocket(
+ address_list_, NULL, net::NetLog::Source());
+ socket_->Connect(net::CompletionCallback());
+
+ run_loop_.reset(new base::RunLoop());
+ PumpLoop();
+
+ DCHECK(socket_->IsConnected());
+ return socket_.get();
+}
+
+void GCMConnectionHandlerImplTest::PumpLoop() {
+ run_loop_->RunUntilIdle();
+ run_loop_.reset(new base::RunLoop());
+}
+
+void GCMConnectionHandlerImplTest::Connect(
+ ScopedMessage* dst_proto) {
+ connection_handler_.reset(new ConnectionHandlerImpl(
+ TestTimeouts::tiny_timeout(),
+ base::Bind(&GCMConnectionHandlerImplTest::ReadContinuation,
+ base::Unretained(this),
+ dst_proto),
+ base::Bind(&GCMConnectionHandlerImplTest::WriteContinuation,
+ base::Unretained(this)),
+ base::Bind(&GCMConnectionHandlerImplTest::ConnectionContinuation,
+ base::Unretained(this))));
+ EXPECT_FALSE(connection_handler()->CanSendMessage());
+ connection_handler_->Init(*BuildLoginRequest(kAuthId, kAuthToken),
+ socket_.Pass());
+}
+
+void GCMConnectionHandlerImplTest::ReadContinuation(
+ ScopedMessage* dst_proto,
+ ScopedMessage new_proto) {
+ *dst_proto = new_proto.Pass();
+ run_loop_->Quit();
+}
+
+void GCMConnectionHandlerImplTest::WaitForMessage() {
+ run_loop_->Run();
+ run_loop_.reset(new base::RunLoop());
+}
+
+void GCMConnectionHandlerImplTest::WriteContinuation() {
+ run_loop_->Quit();
+}
+
+void GCMConnectionHandlerImplTest::ConnectionContinuation(int error) {
+ last_error_ = error;
+ run_loop_->Quit();
+}
+
+// Initialize the connection handler and ensure the handshake completes
+// successfully.
+TEST_F(GCMConnectionHandlerImplTest, Init) {
+ std::string handshake_request = EncodeHandshakeRequest();
+ WriteList write_list(1, net::MockWrite(net::ASYNC,
+ handshake_request.c_str(),
+ handshake_request.size()));
+ std::string handshake_response = EncodeHandshakeResponse();
+ ReadList read_list(1, net::MockRead(net::ASYNC,
+ handshake_response.c_str(),
+ handshake_response.size()));
+ BuildSocket(read_list, write_list);
+
+ ScopedMessage received_message;
+ Connect(&received_message);
+ EXPECT_FALSE(connection_handler()->CanSendMessage());
+ WaitForMessage(); // The login send.
+ WaitForMessage(); // The login response.
+ ASSERT_TRUE(received_message.get());
+ EXPECT_EQ(BuildLoginResponse(), received_message->SerializeAsString());
+ EXPECT_TRUE(connection_handler()->CanSendMessage());
+}
+
+// Simulate the handshake response returning an older version. Initialization
+// should fail.
+TEST_F(GCMConnectionHandlerImplTest, InitFailedVersionCheck) {
+ std::string handshake_request = EncodeHandshakeRequest();
+ WriteList write_list(1, net::MockWrite(net::ASYNC,
+ handshake_request.c_str(),
+ handshake_request.size()));
+ std::string handshake_response = EncodeHandshakeResponse();
+ // Overwrite the version byte.
+ handshake_response[0] = 37;
+ ReadList read_list(1, net::MockRead(net::ASYNC,
+ handshake_response.c_str(),
+ handshake_response.size()));
+ BuildSocket(read_list, write_list);
+
+ ScopedMessage received_message;
+ Connect(&received_message);
+ WaitForMessage(); // The login send.
+ WaitForMessage(); // The login response. Should result in a connection error.
+ EXPECT_FALSE(received_message.get());
+ EXPECT_FALSE(connection_handler()->CanSendMessage());
+ EXPECT_EQ(net::ERR_FAILED, last_error());
+}
+
+// Attempt to initialize, but receive no server response, resulting in a time
+// out.
+TEST_F(GCMConnectionHandlerImplTest, InitTimeout) {
+ std::string handshake_request = EncodeHandshakeRequest();
+ WriteList write_list(1, net::MockWrite(net::ASYNC,
+ handshake_request.c_str(),
+ handshake_request.size()));
+ ReadList read_list(1, net::MockRead(net::SYNCHRONOUS,
+ net::ERR_IO_PENDING));
+ BuildSocket(read_list, write_list);
+
+ ScopedMessage received_message;
+ Connect(&received_message);
+ WaitForMessage(); // The login send.
+ WaitForMessage(); // The login response. Should result in a connection error.
+ EXPECT_FALSE(received_message.get());
+ EXPECT_FALSE(connection_handler()->CanSendMessage());
+ EXPECT_EQ(net::ERR_TIMED_OUT, last_error());
+}
+
+// Attempt to initialize, but receive an incomplete server response, resulting
+// in a time out.
+TEST_F(GCMConnectionHandlerImplTest, InitIncompleteTimeout) {
+ std::string handshake_request = EncodeHandshakeRequest();
+ WriteList write_list(1, net::MockWrite(net::ASYNC,
+ handshake_request.c_str(),
+ handshake_request.size()));
+ std::string handshake_response = EncodeHandshakeResponse();
+ ReadList read_list;
+ read_list.push_back(net::MockRead(net::ASYNC,
+ handshake_response.c_str(),
+ handshake_response.size() / 2));
+ read_list.push_back(net::MockRead(net::SYNCHRONOUS,
+ net::ERR_IO_PENDING));
+ BuildSocket(read_list, write_list);
+
+ ScopedMessage received_message;
+ Connect(&received_message);
+ WaitForMessage(); // The login send.
+ WaitForMessage(); // The login response. Should result in a connection error.
+ EXPECT_FALSE(received_message.get());
+ EXPECT_FALSE(connection_handler()->CanSendMessage());
+ EXPECT_EQ(net::ERR_TIMED_OUT, last_error());
+}
+
+// Reinitialize the connection handler after failing to initialize.
+TEST_F(GCMConnectionHandlerImplTest, ReInit) {
+ std::string handshake_request = EncodeHandshakeRequest();
+ WriteList write_list(1, net::MockWrite(net::ASYNC,
+ handshake_request.c_str(),
+ handshake_request.size()));
+ ReadList read_list(1, net::MockRead(net::SYNCHRONOUS,
+ net::ERR_IO_PENDING));
+ BuildSocket(read_list, write_list);
+
+ ScopedMessage received_message;
+ Connect(&received_message);
+ WaitForMessage(); // The login send.
+ WaitForMessage(); // The login response. Should result in a connection error.
+ EXPECT_FALSE(received_message.get());
+ EXPECT_FALSE(connection_handler()->CanSendMessage());
+ EXPECT_EQ(net::ERR_TIMED_OUT, last_error());
+
+ // Build a new socket and reconnect, successfully this time.
+ std::string handshake_response = EncodeHandshakeResponse();
+ read_list[0] = net::MockRead(net::ASYNC,
+ handshake_response.c_str(),
+ handshake_response.size());
+ BuildSocket(read_list, write_list);
+ Connect(&received_message);
+ EXPECT_FALSE(connection_handler()->CanSendMessage());
+ WaitForMessage(); // The login send.
+ WaitForMessage(); // The login response.
+ ASSERT_TRUE(received_message.get());
+ EXPECT_EQ(BuildLoginResponse(), received_message->SerializeAsString());
+ EXPECT_TRUE(connection_handler()->CanSendMessage());
+}
+
+// Verify that messages can be received after initialization.
+TEST_F(GCMConnectionHandlerImplTest, RecvMsg) {
+ std::string handshake_request = EncodeHandshakeRequest();
+ WriteList write_list(1, net::MockWrite(net::ASYNC,
+ handshake_request.c_str(),
+ handshake_request.size()));
+ std::string handshake_response = EncodeHandshakeResponse();
+
+ std::string data_message_proto = BuildDataMessage(kDataMsgFrom,
+ kDataMsgCategory);
+ std::string data_message_pkt =
+ EncodePacket(kDataMessageStanzaTag, data_message_proto);
+ ReadList read_list;
+ read_list.push_back(net::MockRead(net::ASYNC,
+ handshake_response.c_str(),
+ handshake_response.size()));
+ read_list.push_back(net::MockRead(net::ASYNC,
+ data_message_pkt.c_str(),
+ data_message_pkt.size()));
+ BuildSocket(read_list, write_list);
+
+ ScopedMessage received_message;
+ Connect(&received_message);
+ WaitForMessage(); // The login send.
+ WaitForMessage(); // The login response.
+ WaitForMessage(); // The data message.
+ ASSERT_TRUE(received_message.get());
+ EXPECT_EQ(data_message_proto, received_message->SerializeAsString());
+}
+
+// Verify that if two messages arrive at once, they're treated appropriately.
+TEST_F(GCMConnectionHandlerImplTest, Recv2Msgs) {
+ std::string handshake_request = EncodeHandshakeRequest();
+ WriteList write_list(1, net::MockWrite(net::ASYNC,
+ handshake_request.c_str(),
+ handshake_request.size()));
+ std::string handshake_response = EncodeHandshakeResponse();
+
+ std::string data_message_proto = BuildDataMessage(kDataMsgFrom,
+ kDataMsgCategory);
+ std::string data_message_proto2 = BuildDataMessage(kDataMsgFrom2,
+ kDataMsgCategory2);
+ std::string data_message_pkt =
+ EncodePacket(kDataMessageStanzaTag, data_message_proto);
+ data_message_pkt += EncodePacket(kDataMessageStanzaTag, data_message_proto2);
+ ReadList read_list;
+ read_list.push_back(net::MockRead(net::ASYNC,
+ handshake_response.c_str(),
+ handshake_response.size()));
+ read_list.push_back(net::MockRead(net::SYNCHRONOUS,
+ data_message_pkt.c_str(),
+ data_message_pkt.size()));
+ BuildSocket(read_list, write_list);
+
+ ScopedMessage received_message;
+ Connect(&received_message);
+ WaitForMessage(); // The login send.
+ WaitForMessage(); // The login response.
+ WaitForMessage(); // The first data message.
+ ASSERT_TRUE(received_message.get());
+ EXPECT_EQ(data_message_proto, received_message->SerializeAsString());
+ received_message.reset();
+ WaitForMessage(); // The second data message.
+ ASSERT_TRUE(received_message.get());
+ EXPECT_EQ(data_message_proto2, received_message->SerializeAsString());
+}
+
+// Receive a long (>128 bytes) message.
+TEST_F(GCMConnectionHandlerImplTest, RecvLongMsg) {
+ std::string handshake_request = EncodeHandshakeRequest();
+ WriteList write_list(1, net::MockWrite(net::ASYNC,
+ handshake_request.c_str(),
+ handshake_request.size()));
+ std::string handshake_response = EncodeHandshakeResponse();
+
+ std::string data_message_proto =
+ BuildDataMessage(kDataMsgFromLong, kDataMsgCategoryLong);
+ std::string data_message_pkt =
+ EncodePacket(kDataMessageStanzaTag, data_message_proto);
+ DCHECK_GT(data_message_pkt.size(), 128U);
+ ReadList read_list;
+ read_list.push_back(net::MockRead(net::ASYNC,
+ handshake_response.c_str(),
+ handshake_response.size()));
+ read_list.push_back(net::MockRead(net::ASYNC,
+ data_message_pkt.c_str(),
+ data_message_pkt.size()));
+ BuildSocket(read_list, write_list);
+
+ ScopedMessage received_message;
+ Connect(&received_message);
+ WaitForMessage(); // The login send.
+ WaitForMessage(); // The login response.
+ WaitForMessage(); // The data message.
+ ASSERT_TRUE(received_message.get());
+ EXPECT_EQ(data_message_proto, received_message->SerializeAsString());
+}
+
+// Receive two long (>128 bytes) message.
+TEST_F(GCMConnectionHandlerImplTest, Recv2LongMsgs) {
+ std::string handshake_request = EncodeHandshakeRequest();
+ WriteList write_list(1, net::MockWrite(net::ASYNC,
+ handshake_request.c_str(),
+ handshake_request.size()));
+ std::string handshake_response = EncodeHandshakeResponse();
+
+ std::string data_message_proto =
+ BuildDataMessage(kDataMsgFromLong, kDataMsgCategoryLong);
+ std::string data_message_proto2 =
+ BuildDataMessage(kDataMsgFromLong2, kDataMsgCategoryLong2);
+ std::string data_message_pkt =
+ EncodePacket(kDataMessageStanzaTag, data_message_proto);
+ data_message_pkt += EncodePacket(kDataMessageStanzaTag, data_message_proto2);
+ DCHECK_GT(data_message_pkt.size(), 256U);
+ ReadList read_list;
+ read_list.push_back(net::MockRead(net::ASYNC,
+ handshake_response.c_str(),
+ handshake_response.size()));
+ read_list.push_back(net::MockRead(net::SYNCHRONOUS,
+ data_message_pkt.c_str(),
+ data_message_pkt.size()));
+ BuildSocket(read_list, write_list);
+
+ ScopedMessage received_message;
+ Connect(&received_message);
+ WaitForMessage(); // The login send.
+ WaitForMessage(); // The login response.
+ WaitForMessage(); // The first data message.
+ ASSERT_TRUE(received_message.get());
+ EXPECT_EQ(data_message_proto, received_message->SerializeAsString());
+ received_message.reset();
+ WaitForMessage(); // The second data message.
+ ASSERT_TRUE(received_message.get());
+ EXPECT_EQ(data_message_proto2, received_message->SerializeAsString());
+}
+
+// Simulate a message where the end of the data does not arrive in time and the
+// read times out.
+TEST_F(GCMConnectionHandlerImplTest, ReadTimeout) {
+ std::string handshake_request = EncodeHandshakeRequest();
+ WriteList write_list(1, net::MockWrite(net::ASYNC,
+ handshake_request.c_str(),
+ handshake_request.size()));
+ std::string handshake_response = EncodeHandshakeResponse();
+
+ std::string data_message_proto = BuildDataMessage(kDataMsgFrom,
+ kDataMsgCategory);
+ std::string data_message_pkt =
+ EncodePacket(kDataMessageStanzaTag, data_message_proto);
+ int bytes_in_first_message = data_message_pkt.size() / 2;
+ ReadList read_list;
+ read_list.push_back(net::MockRead(net::ASYNC,
+ handshake_response.c_str(),
+ handshake_response.size()));
+ read_list.push_back(net::MockRead(net::ASYNC,
+ data_message_pkt.c_str(),
+ bytes_in_first_message));
+ read_list.push_back(net::MockRead(net::SYNCHRONOUS,
+ net::ERR_IO_PENDING));
+ read_list.push_back(net::MockRead(net::ASYNC,
+ data_message_pkt.c_str() +
+ bytes_in_first_message,
+ data_message_pkt.size() -
+ bytes_in_first_message));
+ BuildSocket(read_list, write_list);
+
+ ScopedMessage received_message;
+ Connect(&received_message);
+ WaitForMessage(); // The login send.
+ WaitForMessage(); // The login response.
+ received_message.reset();
+ WaitForMessage(); // Should time out.
+ EXPECT_FALSE(received_message.get());
+ EXPECT_EQ(net::ERR_TIMED_OUT, last_error());
+ EXPECT_FALSE(connection_handler()->CanSendMessage());
+
+ // Finish the socket read. Should have no effect.
+ data_provider()->ForceNextRead();
+}
+
+// Receive a message with zero data bytes.
+TEST_F(GCMConnectionHandlerImplTest, RecvMsgNoData) {
+ std::string handshake_request = EncodeHandshakeRequest();
+ WriteList write_list(1, net::MockWrite(net::ASYNC,
+ handshake_request.c_str(),
+ handshake_request.size()));
+ std::string handshake_response = EncodeHandshakeResponse();
+
+ std::string data_message_pkt = EncodePacket(kHeartbeatPingTag, "");
+ ASSERT_EQ(data_message_pkt.size(), 2U);
+ ReadList read_list;
+ read_list.push_back(net::MockRead(net::ASYNC,
+ handshake_response.c_str(),
+ handshake_response.size()));
+ read_list.push_back(net::MockRead(net::ASYNC,
+ data_message_pkt.c_str(),
+ data_message_pkt.size()));
+ BuildSocket(read_list, write_list);
+
+ ScopedMessage received_message;
+ Connect(&received_message);
+ WaitForMessage(); // The login send.
+ WaitForMessage(); // The login response.
+ received_message.reset();
+ WaitForMessage(); // The heartbeat ping.
+ EXPECT_TRUE(received_message.get());
+ EXPECT_EQ(GetMCSProtoTag(*received_message), kHeartbeatPingTag);
+ EXPECT_EQ(net::OK, last_error());
+ EXPECT_TRUE(connection_handler()->CanSendMessage());
+}
+
+// Send a message after performing the handshake.
+TEST_F(GCMConnectionHandlerImplTest, SendMsg) {
+ mcs_proto::DataMessageStanza data_message;
+ data_message.set_from(kDataMsgFrom);
+ data_message.set_category(kDataMsgCategory);
+ std::string handshake_request = EncodeHandshakeRequest();
+ std::string data_message_pkt =
+ EncodePacket(kDataMessageStanzaTag, data_message.SerializeAsString());
+ WriteList write_list;
+ write_list.push_back(net::MockWrite(net::ASYNC,
+ handshake_request.c_str(),
+ handshake_request.size()));
+ write_list.push_back(net::MockWrite(net::ASYNC,
+ data_message_pkt.c_str(),
+ data_message_pkt.size()));
+ std::string handshake_response = EncodeHandshakeResponse();
+ ReadList read_list;
+ read_list.push_back(net::MockRead(net::ASYNC,
+ handshake_response.c_str(),
+ handshake_response.size()));
+ read_list.push_back(net::MockRead(net::SYNCHRONOUS, net::ERR_IO_PENDING));
+ BuildSocket(read_list, write_list);
+
+ ScopedMessage received_message;
+ Connect(&received_message);
+ WaitForMessage(); // The login send.
+ WaitForMessage(); // The login response.
+ EXPECT_TRUE(connection_handler()->CanSendMessage());
+ connection_handler()->SendMessage(data_message);
+ EXPECT_FALSE(connection_handler()->CanSendMessage());
+ WaitForMessage(); // The message send.
+ EXPECT_TRUE(connection_handler()->CanSendMessage());
+}
+
+// Attempt to send a message after the socket is disconnected due to a timeout.
+TEST_F(GCMConnectionHandlerImplTest, SendMsgSocketDisconnected) {
+ std::string handshake_request = EncodeHandshakeRequest();
+ WriteList write_list;
+ write_list.push_back(net::MockWrite(net::ASYNC,
+ handshake_request.c_str(),
+ handshake_request.size()));
+ std::string handshake_response = EncodeHandshakeResponse();
+ ReadList read_list;
+ read_list.push_back(net::MockRead(net::ASYNC,
+ handshake_response.c_str(),
+ handshake_response.size()));
+ read_list.push_back(net::MockRead(net::SYNCHRONOUS, net::ERR_IO_PENDING));
+ net::StreamSocket* socket = BuildSocket(read_list, write_list);
+
+ ScopedMessage received_message;
+ Connect(&received_message);
+ WaitForMessage(); // The login send.
+ WaitForMessage(); // The login response.
+ EXPECT_TRUE(connection_handler()->CanSendMessage());
+ socket->Disconnect();
+ mcs_proto::DataMessageStanza data_message;
+ data_message.set_from(kDataMsgFrom);
+ data_message.set_category(kDataMsgCategory);
+ connection_handler()->SendMessage(data_message);
+ EXPECT_FALSE(connection_handler()->CanSendMessage());
+ WaitForMessage(); // The message send. Should result in an error
+ EXPECT_FALSE(connection_handler()->CanSendMessage());
+ EXPECT_EQ(net::ERR_CONNECTION_CLOSED, last_error());
+}
+
+} // namespace
+} // namespace gcm
diff --git a/chromium/google_apis/gcm/engine/fake_connection_factory.cc b/chromium/google_apis/gcm/engine/fake_connection_factory.cc
new file mode 100644
index 00000000000..54b3423b2d5
--- /dev/null
+++ b/chromium/google_apis/gcm/engine/fake_connection_factory.cc
@@ -0,0 +1,46 @@
+// Copyright 2013 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "google_apis/gcm/engine/fake_connection_factory.h"
+
+#include "google_apis/gcm/engine/fake_connection_handler.h"
+#include "google_apis/gcm/protocol/mcs.pb.h"
+#include "net/socket/stream_socket.h"
+
+namespace gcm {
+
+FakeConnectionFactory::FakeConnectionFactory() {
+}
+
+FakeConnectionFactory::~FakeConnectionFactory() {
+}
+
+void FakeConnectionFactory::Initialize(
+ const BuildLoginRequestCallback& request_builder,
+ const ConnectionHandler::ProtoReceivedCallback& read_callback,
+ const ConnectionHandler::ProtoSentCallback& write_callback) {
+ request_builder_ = request_builder;
+ connection_handler_.reset(new FakeConnectionHandler(read_callback,
+ write_callback));
+}
+
+ConnectionHandler* FakeConnectionFactory::GetConnectionHandler() const {
+ return connection_handler_.get();
+}
+
+void FakeConnectionFactory::Connect() {
+ mcs_proto::LoginRequest login_request;
+ request_builder_.Run(&login_request);
+ connection_handler_->Init(login_request, scoped_ptr<net::StreamSocket>());
+}
+
+bool FakeConnectionFactory::IsEndpointReachable() const {
+ return connection_handler_.get() && connection_handler_->CanSendMessage();
+}
+
+base::TimeTicks FakeConnectionFactory::NextRetryAttempt() const {
+ return base::TimeTicks();
+}
+
+} // namespace gcm
diff --git a/chromium/google_apis/gcm/engine/fake_connection_factory.h b/chromium/google_apis/gcm/engine/fake_connection_factory.h
new file mode 100644
index 00000000000..60b10e130db
--- /dev/null
+++ b/chromium/google_apis/gcm/engine/fake_connection_factory.h
@@ -0,0 +1,42 @@
+// Copyright 2013 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef GOOGLE_APIS_GCM_ENGINE_FAKE_CONNECTION_FACTORY_H_
+#define GOOGLE_APIS_GCM_ENGINE_FAKE_CONNECTION_FACTORY_H_
+
+#include "base/memory/scoped_ptr.h"
+#include "google_apis/gcm/engine/connection_factory.h"
+
+namespace gcm {
+
+class FakeConnectionHandler;
+
+// A connection factory that mocks out real connections, using a fake connection
+// handler instead.
+class FakeConnectionFactory : public ConnectionFactory {
+ public:
+ FakeConnectionFactory();
+ virtual ~FakeConnectionFactory();
+
+ // ConnectionFactory implementation.
+ virtual void Initialize(
+ const BuildLoginRequestCallback& request_builder,
+ const ConnectionHandler::ProtoReceivedCallback& read_callback,
+ const ConnectionHandler::ProtoSentCallback& write_callback) OVERRIDE;
+ virtual ConnectionHandler* GetConnectionHandler() const OVERRIDE;
+ virtual void Connect() OVERRIDE;
+ virtual bool IsEndpointReachable() const OVERRIDE;
+ virtual base::TimeTicks NextRetryAttempt() const OVERRIDE;
+
+ private:
+ scoped_ptr<FakeConnectionHandler> connection_handler_;
+
+ BuildLoginRequestCallback request_builder_;
+
+ DISALLOW_COPY_AND_ASSIGN(FakeConnectionFactory);
+};
+
+} // namespace gcm
+
+#endif // GOOGLE_APIS_GCM_ENGINE_FAKE_CONNECTION_FACTORY_H_
diff --git a/chromium/google_apis/gcm/engine/fake_connection_handler.cc b/chromium/google_apis/gcm/engine/fake_connection_handler.cc
new file mode 100644
index 00000000000..06639331ebe
--- /dev/null
+++ b/chromium/google_apis/gcm/engine/fake_connection_handler.cc
@@ -0,0 +1,86 @@
+// Copyright 2013 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "google_apis/gcm/engine/fake_connection_handler.h"
+
+#include "base/logging.h"
+#include "google_apis/gcm/base/mcs_util.h"
+#include "net/socket/stream_socket.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+namespace gcm {
+
+namespace {
+
+// Build a basic login response.
+scoped_ptr<google::protobuf::MessageLite> BuildLoginResponse(bool fail_login) {
+ scoped_ptr<mcs_proto::LoginResponse> login_response(
+ new mcs_proto::LoginResponse());
+ login_response->set_id("id");
+ if (fail_login)
+ login_response->mutable_error()->set_code(1);
+ return login_response.PassAs<google::protobuf::MessageLite>();
+}
+
+} // namespace
+
+FakeConnectionHandler::FakeConnectionHandler(
+ const ConnectionHandler::ProtoReceivedCallback& read_callback,
+ const ConnectionHandler::ProtoSentCallback& write_callback)
+ : read_callback_(read_callback),
+ write_callback_(write_callback),
+ fail_login_(false),
+ fail_send_(false),
+ initialized_(false) {
+}
+
+FakeConnectionHandler::~FakeConnectionHandler() {
+}
+
+void FakeConnectionHandler::Init(const mcs_proto::LoginRequest& login_request,
+ scoped_ptr<net::StreamSocket> socket) {
+ EXPECT_EQ(expected_outgoing_messages_.front().SerializeAsString(),
+ login_request.SerializeAsString());
+ expected_outgoing_messages_.pop_front();
+ DVLOG(1) << "Received init call.";
+ read_callback_.Run(BuildLoginResponse(fail_login_));
+ initialized_ = !fail_login_;
+}
+
+bool FakeConnectionHandler::CanSendMessage() const {
+ return initialized_;
+}
+
+void FakeConnectionHandler::SendMessage(
+ const google::protobuf::MessageLite& message) {
+ if (expected_outgoing_messages_.empty())
+ FAIL() << "Unexpected message sent.";
+ EXPECT_EQ(expected_outgoing_messages_.front().SerializeAsString(),
+ message.SerializeAsString());
+ expected_outgoing_messages_.pop_front();
+ DVLOG(1) << "Received message, "
+ << (fail_send_ ? " failing send." : "calling back.");
+ if (!fail_send_)
+ write_callback_.Run();
+ else
+ initialized_ = false; // Prevent future messages until reconnect.
+}
+
+void FakeConnectionHandler::ExpectOutgoingMessage(const MCSMessage& message) {
+ expected_outgoing_messages_.push_back(message);
+}
+
+void FakeConnectionHandler::ResetOutgoingMessageExpectations() {
+ expected_outgoing_messages_.clear();
+}
+
+bool FakeConnectionHandler::AllOutgoingMessagesReceived() const {
+ return expected_outgoing_messages_.empty();
+}
+
+void FakeConnectionHandler::ReceiveMessage(const MCSMessage& message) {
+ read_callback_.Run(message.CloneProtobuf());
+}
+
+} // namespace gcm
diff --git a/chromium/google_apis/gcm/engine/fake_connection_handler.h b/chromium/google_apis/gcm/engine/fake_connection_handler.h
new file mode 100644
index 00000000000..5356b771086
--- /dev/null
+++ b/chromium/google_apis/gcm/engine/fake_connection_handler.h
@@ -0,0 +1,74 @@
+// Copyright 2013 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef GOOGLE_APIS_GCM_ENGINE_FAKE_CONNECTION_HANDLER_H_
+#define GOOGLE_APIS_GCM_ENGINE_FAKE_CONNECTION_HANDLER_H_
+
+#include <list>
+
+#include "google_apis/gcm/base/mcs_message.h"
+#include "google_apis/gcm/engine/connection_handler.h"
+
+namespace gcm {
+
+// A fake implementation of a ConnectionHandler that can arbitrarily receive
+// messages and verify expectations for outgoing messages.
+class FakeConnectionHandler : public ConnectionHandler {
+ public:
+ FakeConnectionHandler(
+ const ConnectionHandler::ProtoReceivedCallback& read_callback,
+ const ConnectionHandler::ProtoSentCallback& write_callback);
+ virtual ~FakeConnectionHandler();
+
+ // ConnectionHandler implementation.
+ virtual void Init(const mcs_proto::LoginRequest& login_request,
+ scoped_ptr<net::StreamSocket> socket) OVERRIDE;
+ virtual bool CanSendMessage() const OVERRIDE;
+ virtual void SendMessage(const google::protobuf::MessageLite& message)
+ OVERRIDE;
+
+ // EXPECT's receipt of |message| via SendMessage(..).
+ void ExpectOutgoingMessage(const MCSMessage& message);
+
+ // Reset the expected outgoing messages.
+ void ResetOutgoingMessageExpectations();
+
+ // Whether all expected outgoing messages have been received;
+ bool AllOutgoingMessagesReceived() const;
+
+ // Passes on |message| to |write_callback_|.
+ void ReceiveMessage(const MCSMessage& message);
+
+ // Whether to return an error with the next login response.
+ void set_fail_login(bool fail_login) {
+ fail_login_ = fail_login;
+ }
+
+ // Whether to invoke the write callback on the next send attempt or fake a
+ // connection error instead.
+ void set_fail_send(bool fail_send) {
+ fail_send_ = fail_send;
+ }
+
+ private:
+ ConnectionHandler::ProtoReceivedCallback read_callback_;
+ ConnectionHandler::ProtoSentCallback write_callback_;
+
+ std::list<MCSMessage> expected_outgoing_messages_;
+
+ // Whether to fail the login or not.
+ bool fail_login_;
+
+ // Whether to fail a SendMessage call or not.
+ bool fail_send_;
+
+ // Whether a successful login has completed.
+ bool initialized_;
+
+ DISALLOW_COPY_AND_ASSIGN(FakeConnectionHandler);
+};
+
+} // namespace gcm
+
+#endif // GOOGLE_APIS_GCM_ENGINE_FAKE_CONNECTION_HANDLER_H_
diff --git a/chromium/google_apis/gcm/engine/mcs_client.cc b/chromium/google_apis/gcm/engine/mcs_client.cc
new file mode 100644
index 00000000000..f0af051379f
--- /dev/null
+++ b/chromium/google_apis/gcm/engine/mcs_client.cc
@@ -0,0 +1,659 @@
+// Copyright 2013 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "google_apis/gcm/engine/mcs_client.h"
+
+#include "base/basictypes.h"
+#include "base/message_loop/message_loop.h"
+#include "base/strings/string_number_conversions.h"
+#include "google_apis/gcm/base/mcs_util.h"
+#include "google_apis/gcm/base/socket_stream.h"
+#include "google_apis/gcm/engine/connection_factory.h"
+#include "google_apis/gcm/engine/rmq_store.h"
+
+using namespace google::protobuf::io;
+
+namespace gcm {
+
+namespace {
+
+typedef scoped_ptr<google::protobuf::MessageLite> MCSProto;
+
+// TODO(zea): get these values from MCS settings.
+const int64 kHeartbeatDefaultSeconds = 60 * 15; // 15 minutes.
+
+// The category of messages intended for the GCM client itself from MCS.
+const char kMCSCategory[] = "com.google.android.gsf.gtalkservice";
+
+// The from field for messages originating in the GCM client.
+const char kGCMFromField[] = "gcm@android.com";
+
+// MCS status message types.
+const char kIdleNotification[] = "IdleNotification";
+// TODO(zea): consume the following message types:
+// const char kAlwaysShowOnIdle[] = "ShowAwayOnIdle";
+// const char kPowerNotification[] = "PowerNotification";
+// const char kDataActiveNotification[] = "DataActiveNotification";
+
+// The number of unacked messages to allow before sending a stream ack.
+// Applies to both incoming and outgoing messages.
+// TODO(zea): make this server configurable.
+const int kUnackedMessageBeforeStreamAck = 10;
+
+// The global maximum number of pending messages to have in the send queue.
+const size_t kMaxSendQueueSize = 10 * 1024;
+
+// The maximum message size that can be sent to the server.
+const int kMaxMessageBytes = 4 * 1024; // 4KB, like the server.
+
+// Helper for converting a proto persistent id list to a vector of strings.
+bool BuildPersistentIdListFromProto(const google::protobuf::string& bytes,
+ std::vector<std::string>* id_list) {
+ mcs_proto::SelectiveAck selective_ack;
+ if (!selective_ack.ParseFromString(bytes))
+ return false;
+ std::vector<std::string> new_list;
+ for (int i = 0; i < selective_ack.id_size(); ++i) {
+ DCHECK(!selective_ack.id(i).empty());
+ new_list.push_back(selective_ack.id(i));
+ }
+ id_list->swap(new_list);
+ return true;
+}
+
+} // namespace
+
+struct ReliablePacketInfo {
+ ReliablePacketInfo();
+ ~ReliablePacketInfo();
+
+ // The stream id with which the message was sent.
+ uint32 stream_id;
+
+ // If reliable delivery was requested, the persistent id of the message.
+ std::string persistent_id;
+
+ // The type of message itself (for easier lookup).
+ uint8 tag;
+
+ // The protobuf of the message itself.
+ MCSProto protobuf;
+};
+
+ReliablePacketInfo::ReliablePacketInfo()
+ : stream_id(0), tag(0) {
+}
+ReliablePacketInfo::~ReliablePacketInfo() {}
+
+MCSClient::MCSClient(
+ const base::FilePath& rmq_path,
+ ConnectionFactory* connection_factory,
+ scoped_refptr<base::SequencedTaskRunner> blocking_task_runner)
+ : state_(UNINITIALIZED),
+ android_id_(0),
+ security_token_(0),
+ connection_factory_(connection_factory),
+ connection_handler_(NULL),
+ last_device_to_server_stream_id_received_(0),
+ last_server_to_device_stream_id_received_(0),
+ stream_id_out_(0),
+ stream_id_in_(0),
+ rmq_store_(rmq_path, blocking_task_runner),
+ heartbeat_interval_(
+ base::TimeDelta::FromSeconds(kHeartbeatDefaultSeconds)),
+ heartbeat_timer_(true, true),
+ blocking_task_runner_(blocking_task_runner),
+ weak_ptr_factory_(this) {
+}
+
+MCSClient::~MCSClient() {
+}
+
+void MCSClient::Initialize(
+ const InitializationCompleteCallback& initialization_callback,
+ const OnMessageReceivedCallback& message_received_callback,
+ const OnMessageSentCallback& message_sent_callback) {
+ DCHECK_EQ(state_, UNINITIALIZED);
+ initialization_callback_ = initialization_callback;
+ message_received_callback_ = message_received_callback;
+ message_sent_callback_ = message_sent_callback;
+
+ state_ = LOADING;
+ rmq_store_.Load(base::Bind(&MCSClient::OnRMQLoadFinished,
+ weak_ptr_factory_.GetWeakPtr()));
+
+ connection_factory_->Initialize(
+ base::Bind(&MCSClient::ResetStateAndBuildLoginRequest,
+ weak_ptr_factory_.GetWeakPtr()),
+ base::Bind(&MCSClient::HandlePacketFromWire,
+ weak_ptr_factory_.GetWeakPtr()),
+ base::Bind(&MCSClient::MaybeSendMessage,
+ weak_ptr_factory_.GetWeakPtr()));
+ connection_handler_ = connection_factory_->GetConnectionHandler();
+}
+
+void MCSClient::Login(uint64 android_id, uint64 security_token) {
+ DCHECK_EQ(state_, LOADED);
+ if (android_id != android_id_ && security_token != security_token_) {
+ DCHECK(android_id);
+ DCHECK(security_token);
+ DCHECK(restored_unackeds_server_ids_.empty());
+ android_id_ = android_id;
+ security_token_ = security_token;
+ rmq_store_.SetDeviceCredentials(android_id_,
+ security_token_,
+ base::Bind(&MCSClient::OnRMQUpdateFinished,
+ weak_ptr_factory_.GetWeakPtr()));
+ }
+
+ state_ = CONNECTING;
+ connection_factory_->Connect();
+}
+
+void MCSClient::SendMessage(const MCSMessage& message, bool use_rmq) {
+ DCHECK_EQ(state_, CONNECTED);
+ if (to_send_.size() > kMaxSendQueueSize) {
+ base::MessageLoop::current()->PostTask(
+ FROM_HERE,
+ base::Bind(message_sent_callback_, "Message queue full."));
+ return;
+ }
+ if (message.size() > kMaxMessageBytes) {
+ base::MessageLoop::current()->PostTask(
+ FROM_HERE,
+ base::Bind(message_sent_callback_, "Message too large."));
+ return;
+ }
+
+ ReliablePacketInfo* packet_info = new ReliablePacketInfo();
+ packet_info->protobuf = message.CloneProtobuf();
+
+ if (use_rmq) {
+ PersistentId persistent_id = GetNextPersistentId();
+ DVLOG(1) << "Setting persistent id to " << persistent_id;
+ packet_info->persistent_id = persistent_id;
+ SetPersistentId(persistent_id,
+ packet_info->protobuf.get());
+ rmq_store_.AddOutgoingMessage(persistent_id,
+ MCSMessage(message.tag(),
+ *(packet_info->protobuf)),
+ base::Bind(&MCSClient::OnRMQUpdateFinished,
+ weak_ptr_factory_.GetWeakPtr()));
+ } else {
+ // Check that there is an active connection to the endpoint.
+ if (!connection_handler_->CanSendMessage()) {
+ base::MessageLoop::current()->PostTask(
+ FROM_HERE,
+ base::Bind(message_sent_callback_, "Unable to reach endpoint"));
+ return;
+ }
+ }
+ to_send_.push_back(make_linked_ptr(packet_info));
+ MaybeSendMessage();
+}
+
+void MCSClient::Destroy() {
+ rmq_store_.Destroy(base::Bind(&MCSClient::OnRMQUpdateFinished,
+ weak_ptr_factory_.GetWeakPtr()));
+}
+
+void MCSClient::ResetStateAndBuildLoginRequest(
+ mcs_proto::LoginRequest* request) {
+ DCHECK(android_id_);
+ DCHECK(security_token_);
+ stream_id_in_ = 0;
+ stream_id_out_ = 1;
+ last_device_to_server_stream_id_received_ = 0;
+ last_server_to_device_stream_id_received_ = 0;
+
+ // TODO(zea): expire all messages older than their TTL.
+
+ // Add any pending acknowledgments to the list of ids.
+ for (StreamIdToPersistentIdMap::const_iterator iter =
+ unacked_server_ids_.begin();
+ iter != unacked_server_ids_.end(); ++iter) {
+ restored_unackeds_server_ids_.push_back(iter->second);
+ }
+ unacked_server_ids_.clear();
+
+ // Any acknowledged server ids which have not been confirmed by the server
+ // are treated like unacknowledged ids.
+ for (std::map<StreamId, PersistentIdList>::const_iterator iter =
+ acked_server_ids_.begin();
+ iter != acked_server_ids_.end(); ++iter) {
+ restored_unackeds_server_ids_.insert(restored_unackeds_server_ids_.end(),
+ iter->second.begin(),
+ iter->second.end());
+ }
+ acked_server_ids_.clear();
+
+ // Then build the request, consuming all pending acknowledgments.
+ request->Swap(BuildLoginRequest(android_id_, security_token_).get());
+ for (PersistentIdList::const_iterator iter =
+ restored_unackeds_server_ids_.begin();
+ iter != restored_unackeds_server_ids_.end(); ++iter) {
+ request->add_received_persistent_id(*iter);
+ }
+ acked_server_ids_[stream_id_out_] = restored_unackeds_server_ids_;
+ restored_unackeds_server_ids_.clear();
+
+ // Push all unacknowledged messages to front of send queue. No need to save
+ // to RMQ, as all messages that reach this point should already have been
+ // saved as necessary.
+ while (!to_resend_.empty()) {
+ to_send_.push_front(to_resend_.back());
+ to_resend_.pop_back();
+ }
+ DVLOG(1) << "Resetting state, with " << request->received_persistent_id_size()
+ << " incoming acks pending, and " << to_send_.size()
+ << " pending outgoing messages.";
+
+ heartbeat_timer_.Stop();
+
+ state_ = CONNECTING;
+}
+
+void MCSClient::SendHeartbeat() {
+ SendMessage(MCSMessage(kHeartbeatPingTag, mcs_proto::HeartbeatPing()),
+ false);
+}
+
+void MCSClient::OnRMQLoadFinished(const RMQStore::LoadResult& result) {
+ if (!result.success) {
+ state_ = UNINITIALIZED;
+ LOG(ERROR) << "Failed to load/create RMQ state. Not connecting.";
+ initialization_callback_.Run(false, 0, 0);
+ return;
+ }
+ state_ = LOADED;
+ stream_id_out_ = 1; // Login request is hardcoded to id 1.
+
+ if (result.device_android_id == 0 || result.device_security_token == 0) {
+ DVLOG(1) << "No device credentials found, assuming new client.";
+ initialization_callback_.Run(true, 0, 0);
+ return;
+ }
+
+ android_id_ = result.device_android_id;
+ security_token_ = result.device_security_token;
+
+ DVLOG(1) << "RMQ Load finished with " << result.incoming_messages.size()
+ << " incoming acks pending and " << result.outgoing_messages.size()
+ << " outgoing messages pending.";
+
+ restored_unackeds_server_ids_ = result.incoming_messages;
+
+ // First go through and order the outgoing messages by recency.
+ std::map<uint64, google::protobuf::MessageLite*> ordered_messages;
+ for (std::map<PersistentId, google::protobuf::MessageLite*>::const_iterator
+ iter = result.outgoing_messages.begin();
+ iter != result.outgoing_messages.end(); ++iter) {
+ uint64 timestamp = 0;
+ if (!base::StringToUint64(iter->first, &timestamp)) {
+ LOG(ERROR) << "Invalid restored message.";
+ return;
+ }
+ ordered_messages[timestamp] = iter->second;
+ }
+
+ // Now go through and add the outgoing messages to the send queue in their
+ // appropriate order (oldest at front, most recent at back).
+ for (std::map<uint64, google::protobuf::MessageLite*>::const_iterator
+ iter = ordered_messages.begin();
+ iter != ordered_messages.end(); ++iter) {
+ ReliablePacketInfo* packet_info = new ReliablePacketInfo();
+ packet_info->protobuf.reset(iter->second);
+ packet_info->persistent_id = base::Uint64ToString(iter->first);
+ to_send_.push_back(make_linked_ptr(packet_info));
+ }
+
+ initialization_callback_.Run(true, android_id_, security_token_);
+}
+
+void MCSClient::OnRMQUpdateFinished(bool success) {
+ LOG_IF(ERROR, !success) << "RMQ Update failed!";
+ // TODO(zea): Rebuild the store from scratch in case of persistence failure?
+}
+
+void MCSClient::MaybeSendMessage() {
+ if (to_send_.empty())
+ return;
+
+ if (!connection_handler_->CanSendMessage())
+ return;
+
+ // TODO(zea): drop messages older than their TTL.
+
+ DVLOG(1) << "Pending output message found, sending.";
+ MCSPacketInternal packet = to_send_.front();
+ to_send_.pop_front();
+ if (!packet->persistent_id.empty())
+ to_resend_.push_back(packet);
+ SendPacketToWire(packet.get());
+}
+
+void MCSClient::SendPacketToWire(ReliablePacketInfo* packet_info) {
+ // Reset the heartbeat interval.
+ heartbeat_timer_.Reset();
+ packet_info->stream_id = ++stream_id_out_;
+ DVLOG(1) << "Sending packet of type " << packet_info->protobuf->GetTypeName();
+
+ // Set the proper last received stream id to acknowledge received server
+ // packets.
+ DVLOG(1) << "Setting last stream id received to "
+ << stream_id_in_;
+ SetLastStreamIdReceived(stream_id_in_,
+ packet_info->protobuf.get());
+ if (stream_id_in_ != last_server_to_device_stream_id_received_) {
+ last_server_to_device_stream_id_received_ = stream_id_in_;
+ // Mark all acknowledged server messages as such. Note: they're not dropped,
+ // as it may be that they'll need to be re-acked if this message doesn't
+ // make it.
+ PersistentIdList persistent_id_list;
+ for (StreamIdToPersistentIdMap::const_iterator iter =
+ unacked_server_ids_.begin();
+ iter != unacked_server_ids_.end(); ++iter) {
+ DCHECK_LE(iter->first, last_server_to_device_stream_id_received_);
+ persistent_id_list.push_back(iter->second);
+ }
+ unacked_server_ids_.clear();
+ acked_server_ids_[stream_id_out_] = persistent_id_list;
+ }
+
+ connection_handler_->SendMessage(*packet_info->protobuf);
+}
+
+void MCSClient::HandleMCSDataMesssage(
+ scoped_ptr<google::protobuf::MessageLite> protobuf) {
+ mcs_proto::DataMessageStanza* data_message =
+ reinterpret_cast<mcs_proto::DataMessageStanza*>(protobuf.get());
+ // TODO(zea): implement a proper status manager rather than hardcoding these
+ // values.
+ scoped_ptr<mcs_proto::DataMessageStanza> response(
+ new mcs_proto::DataMessageStanza());
+ response->set_from(kGCMFromField);
+ bool send = false;
+ for (int i = 0; i < data_message->app_data_size(); ++i) {
+ const mcs_proto::AppData& app_data = data_message->app_data(i);
+ if (app_data.key() == kIdleNotification) {
+ // Tell the MCS server the client is not idle.
+ send = true;
+ mcs_proto::AppData data;
+ data.set_key(kIdleNotification);
+ data.set_value("false");
+ response->add_app_data()->CopyFrom(data);
+ response->set_category(kMCSCategory);
+ }
+ }
+
+ if (send) {
+ SendMessage(
+ MCSMessage(kDataMessageStanzaTag,
+ response.PassAs<const google::protobuf::MessageLite>()),
+ false);
+ }
+}
+
+void MCSClient::HandlePacketFromWire(
+ scoped_ptr<google::protobuf::MessageLite> protobuf) {
+ if (!protobuf.get())
+ return;
+ uint8 tag = GetMCSProtoTag(*protobuf);
+ PersistentId persistent_id = GetPersistentId(*protobuf);
+ StreamId last_stream_id_received = GetLastStreamIdReceived(*protobuf);
+
+ if (last_stream_id_received != 0) {
+ last_device_to_server_stream_id_received_ = last_stream_id_received;
+
+ // Process device to server messages that have now been acknowledged by the
+ // server. Because messages are stored in order, just pop off all that have
+ // a stream id lower than server's last received stream id.
+ HandleStreamAck(last_stream_id_received);
+
+ // Process server_to_device_messages that the server now knows were
+ // acknowledged. Again, they're in order, so just keep going until the
+ // stream id is reached.
+ StreamIdList acked_stream_ids_to_remove;
+ for (std::map<StreamId, PersistentIdList>::iterator iter =
+ acked_server_ids_.begin();
+ iter != acked_server_ids_.end() &&
+ iter->first <= last_stream_id_received; ++iter) {
+ acked_stream_ids_to_remove.push_back(iter->first);
+ }
+ for (StreamIdList::iterator iter = acked_stream_ids_to_remove.begin();
+ iter != acked_stream_ids_to_remove.end(); ++iter) {
+ acked_server_ids_.erase(*iter);
+ }
+ }
+
+ ++stream_id_in_;
+ if (!persistent_id.empty()) {
+ unacked_server_ids_[stream_id_in_] = persistent_id;
+ rmq_store_.AddIncomingMessage(persistent_id,
+ base::Bind(&MCSClient::OnRMQUpdateFinished,
+ weak_ptr_factory_.GetWeakPtr()));
+ }
+
+ DVLOG(1) << "Received message of type " << protobuf->GetTypeName()
+ << " with persistent id "
+ << (persistent_id.empty() ? "NULL" : persistent_id)
+ << ", stream id " << stream_id_in_ << " and last stream id received "
+ << last_stream_id_received;
+
+ if (unacked_server_ids_.size() > 0 &&
+ unacked_server_ids_.size() % kUnackedMessageBeforeStreamAck == 0) {
+ SendMessage(MCSMessage(kIqStanzaTag,
+ BuildStreamAck().
+ PassAs<const google::protobuf::MessageLite>()),
+ false);
+ }
+
+ switch (tag) {
+ case kLoginResponseTag: {
+ mcs_proto::LoginResponse* login_response =
+ reinterpret_cast<mcs_proto::LoginResponse*>(protobuf.get());
+ DVLOG(1) << "Received login response:";
+ DVLOG(1) << " Id: " << login_response->id();
+ DVLOG(1) << " Timestamp: " << login_response->server_timestamp();
+ if (login_response->has_error()) {
+ state_ = UNINITIALIZED;
+ DVLOG(1) << " Error code: " << login_response->error().code();
+ DVLOG(1) << " Error message: " << login_response->error().message();
+ initialization_callback_.Run(false, 0, 0);
+ return;
+ }
+
+ state_ = CONNECTED;
+ stream_id_in_ = 1; // To account for the login response.
+ DCHECK_EQ(1U, stream_id_out_);
+
+ // Pass the login response on up.
+ base::MessageLoop::current()->PostTask(
+ FROM_HERE,
+ base::Bind(message_received_callback_,
+ MCSMessage(tag,
+ protobuf.PassAs<
+ const google::protobuf::MessageLite>())));
+
+ // If there are pending messages, attempt to send one.
+ if (!to_send_.empty()) {
+ base::MessageLoop::current()->PostTask(
+ FROM_HERE,
+ base::Bind(&MCSClient::MaybeSendMessage,
+ weak_ptr_factory_.GetWeakPtr()));
+ }
+
+ heartbeat_timer_.Start(FROM_HERE,
+ heartbeat_interval_,
+ base::Bind(&MCSClient::SendHeartbeat,
+ weak_ptr_factory_.GetWeakPtr()));
+ return;
+ }
+ case kHeartbeatPingTag:
+ DCHECK_GE(stream_id_in_, 1U);
+ DVLOG(1) << "Received heartbeat ping, sending ack.";
+ SendMessage(
+ MCSMessage(kHeartbeatAckTag, mcs_proto::HeartbeatAck()), false);
+ return;
+ case kHeartbeatAckTag:
+ DCHECK_GE(stream_id_in_, 1U);
+ DVLOG(1) << "Received heartbeat ack.";
+ // TODO(zea): add logic to reconnect if no ack received within a certain
+ // timeout (with backoff).
+ return;
+ case kCloseTag:
+ LOG(ERROR) << "Received close command, closing connection.";
+ state_ = UNINITIALIZED;
+ initialization_callback_.Run(false, 0, 0);
+ // TODO(zea): should this happen in non-error cases? Reconnect?
+ return;
+ case kIqStanzaTag: {
+ DCHECK_GE(stream_id_in_, 1U);
+ mcs_proto::IqStanza* iq_stanza =
+ reinterpret_cast<mcs_proto::IqStanza*>(protobuf.get());
+ const mcs_proto::Extension& iq_extension = iq_stanza->extension();
+ switch (iq_extension.id()) {
+ case kSelectiveAck: {
+ PersistentIdList acked_ids;
+ if (BuildPersistentIdListFromProto(iq_extension.data(),
+ &acked_ids)) {
+ HandleSelectiveAck(acked_ids);
+ }
+ return;
+ }
+ case kStreamAck:
+ // Do nothing. The last received stream id is always processed if it's
+ // present.
+ return;
+ default:
+ LOG(WARNING) << "Received invalid iq stanza extension "
+ << iq_extension.id();
+ return;
+ }
+ }
+ case kDataMessageStanzaTag: {
+ DCHECK_GE(stream_id_in_, 1U);
+ mcs_proto::DataMessageStanza* data_message =
+ reinterpret_cast<mcs_proto::DataMessageStanza*>(protobuf.get());
+ if (data_message->category() == kMCSCategory) {
+ HandleMCSDataMesssage(protobuf.Pass());
+ return;
+ }
+
+ DCHECK(protobuf.get());
+ base::MessageLoop::current()->PostTask(
+ FROM_HERE,
+ base::Bind(message_received_callback_,
+ MCSMessage(tag,
+ protobuf.PassAs<
+ const google::protobuf::MessageLite>())));
+ return;
+ }
+ default:
+ LOG(ERROR) << "Received unexpected message of type "
+ << static_cast<int>(tag);
+ return;
+ }
+}
+
+void MCSClient::HandleStreamAck(StreamId last_stream_id_received) {
+ PersistentIdList acked_outgoing_persistent_ids;
+ StreamIdList acked_outgoing_stream_ids;
+ while (!to_resend_.empty() &&
+ to_resend_.front()->stream_id <= last_stream_id_received) {
+ const MCSPacketInternal& outgoing_packet = to_resend_.front();
+ acked_outgoing_persistent_ids.push_back(outgoing_packet->persistent_id);
+ acked_outgoing_stream_ids.push_back(outgoing_packet->stream_id);
+ to_resend_.pop_front();
+ }
+
+ DVLOG(1) << "Server acked " << acked_outgoing_persistent_ids.size()
+ << " outgoing messages, " << to_resend_.size()
+ << " remaining unacked";
+ rmq_store_.RemoveOutgoingMessages(acked_outgoing_persistent_ids,
+ base::Bind(&MCSClient::OnRMQUpdateFinished,
+ weak_ptr_factory_.GetWeakPtr()));
+
+ HandleServerConfirmedReceipt(last_stream_id_received);
+}
+
+void MCSClient::HandleSelectiveAck(const PersistentIdList& id_list) {
+ // First check the to_resend_ queue. Acknowledgments should always happen
+ // in the order they were sent, so if messages are present they should match
+ // the acknowledge list.
+ PersistentIdList::const_iterator iter = id_list.begin();
+ for (; iter != id_list.end() && !to_resend_.empty(); ++iter) {
+ const MCSPacketInternal& outgoing_packet = to_resend_.front();
+ DCHECK_EQ(outgoing_packet->persistent_id, *iter);
+
+ // No need to re-acknowledge any server messages this message already
+ // acknowledged.
+ StreamId device_stream_id = outgoing_packet->stream_id;
+ HandleServerConfirmedReceipt(device_stream_id);
+
+ to_resend_.pop_front();
+ }
+
+ // If the acknowledged ids aren't all there, they might be in the to_send_
+ // queue (typically when a StreamAck confirms messages as part of a login
+ // response).
+ for (; iter != id_list.end() && !to_send_.empty(); ++iter) {
+ const MCSPacketInternal& outgoing_packet = to_send_.front();
+ DCHECK_EQ(outgoing_packet->persistent_id, *iter);
+
+ // No need to re-acknowledge any server messages this message already
+ // acknowledged.
+ StreamId device_stream_id = outgoing_packet->stream_id;
+ HandleServerConfirmedReceipt(device_stream_id);
+
+ to_send_.pop_front();
+ }
+
+ DCHECK(iter == id_list.end());
+
+ DVLOG(1) << "Server acked " << id_list.size()
+ << " messages, " << to_resend_.size() << " remaining unacked.";
+ rmq_store_.RemoveOutgoingMessages(id_list,
+ base::Bind(&MCSClient::OnRMQUpdateFinished,
+ weak_ptr_factory_.GetWeakPtr()));
+
+ // Resend any remaining outgoing messages, as they were not received by the
+ // server.
+ DVLOG(1) << "Resending " << to_resend_.size() << " messages.";
+ while (!to_resend_.empty()) {
+ to_send_.push_front(to_resend_.back());
+ to_resend_.pop_back();
+ }
+}
+
+void MCSClient::HandleServerConfirmedReceipt(StreamId device_stream_id) {
+ // TODO(zea): use a message id the sender understands.
+ base::MessageLoop::current()->PostTask(
+ FROM_HERE,
+ base::Bind(message_sent_callback_,
+ "Message " + base::UintToString(device_stream_id) + " sent."));
+
+ PersistentIdList acked_incoming_ids;
+ for (std::map<StreamId, PersistentIdList>::iterator iter =
+ acked_server_ids_.begin();
+ iter != acked_server_ids_.end() &&
+ iter->first <= device_stream_id;) {
+ acked_incoming_ids.insert(acked_incoming_ids.end(),
+ iter->second.begin(),
+ iter->second.end());
+ acked_server_ids_.erase(iter++);
+ }
+
+ DVLOG(1) << "Server confirmed receipt of " << acked_incoming_ids.size()
+ << " acknowledged server messages.";
+ rmq_store_.RemoveIncomingMessages(acked_incoming_ids,
+ base::Bind(&MCSClient::OnRMQUpdateFinished,
+ weak_ptr_factory_.GetWeakPtr()));
+}
+
+MCSClient::PersistentId MCSClient::GetNextPersistentId() {
+ return base::Uint64ToString(base::TimeTicks::Now().ToInternalValue());
+}
+
+} // namespace gcm
diff --git a/chromium/google_apis/gcm/engine/mcs_client.h b/chromium/google_apis/gcm/engine/mcs_client.h
new file mode 100644
index 00000000000..4de62cb127e
--- /dev/null
+++ b/chromium/google_apis/gcm/engine/mcs_client.h
@@ -0,0 +1,231 @@
+// Copyright 2013 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef GOOGLE_APIS_GCM_ENGINE_MCS_CLIENT_H_
+#define GOOGLE_APIS_GCM_ENGINE_MCS_CLIENT_H_
+
+#include <deque>
+#include <map>
+#include <string>
+#include <vector>
+
+#include "base/files/file_path.h"
+#include "base/memory/linked_ptr.h"
+#include "base/memory/weak_ptr.h"
+#include "base/timer/timer.h"
+#include "google_apis/gcm/base/gcm_export.h"
+#include "google_apis/gcm/base/mcs_message.h"
+#include "google_apis/gcm/engine/connection_handler.h"
+#include "google_apis/gcm/engine/rmq_store.h"
+
+namespace google {
+namespace protobuf {
+class MessageLite;
+} // namespace protobuf
+} // namespace google
+
+namespace mcs_proto {
+class LoginRequest;
+}
+
+namespace gcm {
+
+class ConnectionFactory;
+struct ReliablePacketInfo;
+
+// An MCS client. This client is in charge of all communications with an
+// MCS endpoint, and is capable of reliably sending/receiving GCM messages.
+// NOTE: Not thread safe. This class should live on the same thread as that
+// network requests are performed on.
+class GCM_EXPORT MCSClient {
+ public:
+ enum State {
+ UNINITIALIZED, // Uninitialized.
+ LOADING, // Waiting for RMQ load to finish.
+ LOADED, // RMQ Load finished, waiting to connect.
+ CONNECTING, // Connection in progress.
+ CONNECTED, // Connected and running.
+ };
+
+ // Callback for informing MCSClient status. It is valid for this to be
+ // invoked more than once if a permanent error is encountered after a
+ // successful login was initiated.
+ typedef base::Callback<
+ void(bool success,
+ uint64 restored_android_id,
+ uint64 restored_security_token)> InitializationCompleteCallback;
+ // Callback when a message is received.
+ typedef base::Callback<void(const MCSMessage& message)>
+ OnMessageReceivedCallback;
+ // Callback when a message is sent (and receipt has been acknowledged by
+ // the MCS endpoint).
+ // TODO(zea): pass some sort of structure containing more details about
+ // send failures.
+ typedef base::Callback<void(const std::string& message_id)>
+ OnMessageSentCallback;
+
+ MCSClient(const base::FilePath& rmq_path,
+ ConnectionFactory* connection_factory,
+ scoped_refptr<base::SequencedTaskRunner> blocking_task_runner);
+ virtual ~MCSClient();
+
+ // Initialize the client. Will load any previous id/token information as well
+ // as unacknowledged message information from the RMQ storage, if it exists,
+ // passing the id/token information back via |initialization_callback| along
+ // with a |success == true| result. If no RMQ information is present (and
+ // this is therefore a fresh client), a clean RMQ store will be created and
+ // values of 0 will be returned via |initialization_callback| with
+ // |success == true|.
+ /// If an error loading the RMQ store is encountered,
+ // |initialization_callback| will be invoked with |success == false|.
+ void Initialize(const InitializationCompleteCallback& initialization_callback,
+ const OnMessageReceivedCallback& message_received_callback,
+ const OnMessageSentCallback& message_sent_callback);
+
+ // Logs the client into the server. Client must be initialized.
+ // |android_id| and |security_token| are optional if this is not a new
+ // client, else they must be non-zero.
+ // Successful login will result in |message_received_callback| being invoked
+ // with a valid LoginResponse.
+ // Login failure (typically invalid id/token) will shut down the client, and
+ // |initialization_callback| to be invoked with |success = false|.
+ void Login(uint64 android_id, uint64 security_token);
+
+ // Sends a message, with or without reliable message queueing (RMQ) support.
+ // Will asynchronously invoke the OnMessageSent callback regardless.
+ // TODO(zea): support TTL.
+ void SendMessage(const MCSMessage& message, bool use_rmq);
+
+ // Disconnects the client and permanently destroys the persistent RMQ store.
+ // WARNING: This is permanent, and the client must be recreated with new
+ // credentials afterwards.
+ void Destroy();
+
+ // Returns the current state of the client.
+ State state() const { return state_; }
+
+ private:
+ typedef uint32 StreamId;
+ typedef std::string PersistentId;
+ typedef std::vector<StreamId> StreamIdList;
+ typedef std::vector<PersistentId> PersistentIdList;
+ typedef std::map<StreamId, PersistentId> StreamIdToPersistentIdMap;
+ typedef linked_ptr<ReliablePacketInfo> MCSPacketInternal;
+
+ // Resets the internal state and builds a new login request, acknowledging
+ // any pending server-to-device messages and rebuilding the send queue
+ // from all unacknowledged device-to-server messages.
+ // Should only be called when the connection has been reset.
+ void ResetStateAndBuildLoginRequest(mcs_proto::LoginRequest* request);
+
+ // Send a heartbeat to the MCS server.
+ void SendHeartbeat();
+
+ // RMQ Store callbacks.
+ void OnRMQLoadFinished(const RMQStore::LoadResult& result);
+ void OnRMQUpdateFinished(bool success);
+
+ // Attempt to send a message.
+ void MaybeSendMessage();
+
+ // Helper for sending a protobuf along with any unacknowledged ids to the
+ // wire.
+ void SendPacketToWire(ReliablePacketInfo* packet_info);
+
+ // Handle a data message sent to the MCS client system from the MCS server.
+ void HandleMCSDataMesssage(
+ scoped_ptr<google::protobuf::MessageLite> protobuf);
+
+ // Handle a packet received over the wire.
+ void HandlePacketFromWire(scoped_ptr<google::protobuf::MessageLite> protobuf);
+
+ // ReliableMessageQueue acknowledgment helpers.
+ // Handle a StreamAck sent by the server confirming receipt of all
+ // messages up to the message with stream id |last_stream_id_received|.
+ void HandleStreamAck(StreamId last_stream_id_received_);
+ // Handle a SelectiveAck sent by the server confirming all messages
+ // in |id_list|.
+ void HandleSelectiveAck(const PersistentIdList& id_list);
+ // Handle server confirmation of a device message, including device's
+ // acknowledgment of receipt of messages.
+ void HandleServerConfirmedReceipt(StreamId device_stream_id);
+
+ // Generates a new persistent id for messages.
+ // Virtual for testing.
+ virtual PersistentId GetNextPersistentId();
+
+ // Client state.
+ State state_;
+
+ // Callbacks for owner.
+ InitializationCompleteCallback initialization_callback_;
+ OnMessageReceivedCallback message_received_callback_;
+ OnMessageSentCallback message_sent_callback_;
+
+ // The android id and security token in use by this device.
+ uint64 android_id_;
+ uint64 security_token_;
+
+ // Factory for creating new connections and connection handlers.
+ ConnectionFactory* connection_factory_;
+
+ // Connection handler to handle all over-the-wire protocol communication
+ // with the mobile connection server.
+ ConnectionHandler* connection_handler_;
+
+ // ----- Reliablie Message Queue section -----
+ // Note: all queues/maps are ordered from oldest (front/begin) message to
+ // most recent (back/end).
+
+ // Send/acknowledge queues.
+ std::deque<MCSPacketInternal> to_send_;
+ std::deque<MCSPacketInternal> to_resend_;
+
+ // Last device_to_server stream id acknowledged by the server.
+ StreamId last_device_to_server_stream_id_received_;
+ // Last server_to_device stream id acknowledged by this device.
+ StreamId last_server_to_device_stream_id_received_;
+ // The stream id for the last sent message. A new message should consume
+ // stream_id_out_ + 1.
+ StreamId stream_id_out_;
+ // The stream id of the last received message. The LoginResponse will always
+ // have a stream id of 1, and stream ids increment by 1 for each received
+ // message.
+ StreamId stream_id_in_;
+
+ // The server messages that have not been acked by the device yet. Keyed by
+ // server stream id.
+ StreamIdToPersistentIdMap unacked_server_ids_;
+
+ // Those server messages that have been acked. They must remain tracked
+ // until the ack message is itself confirmed. The list of all message ids
+ // acknowledged are keyed off the device stream id of the message that
+ // acknowledged them.
+ std::map<StreamId, PersistentIdList> acked_server_ids_;
+
+ // Those server messages from a previous connection that were not fully
+ // acknowledged. They do not have associated stream ids, and will be
+ // acknowledged on the next login attempt.
+ PersistentIdList restored_unackeds_server_ids_;
+
+ // The reliable message queue persistent store.
+ RMQStore rmq_store_;
+
+ // ----- Heartbeats -----
+ // The current heartbeat interval.
+ base::TimeDelta heartbeat_interval_;
+ // Timer for triggering heartbeats.
+ base::Timer heartbeat_timer_;
+
+ // The task runner for blocking tasks (i.e. persisting RMQ state to disk).
+ scoped_refptr<base::SequencedTaskRunner> blocking_task_runner_;
+
+ base::WeakPtrFactory<MCSClient> weak_ptr_factory_;
+
+ DISALLOW_COPY_AND_ASSIGN(MCSClient);
+};
+
+} // namespace gcm
+
+#endif // GOOGLE_APIS_GCM_ENGINE_MCS_CLIENT_H_
diff --git a/chromium/google_apis/gcm/engine/mcs_client_unittest.cc b/chromium/google_apis/gcm/engine/mcs_client_unittest.cc
new file mode 100644
index 00000000000..6ef140586e9
--- /dev/null
+++ b/chromium/google_apis/gcm/engine/mcs_client_unittest.cc
@@ -0,0 +1,540 @@
+// Copyright 2013 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "google_apis/gcm/engine/mcs_client.h"
+
+#include "base/files/scoped_temp_dir.h"
+#include "base/message_loop/message_loop.h"
+#include "base/run_loop.h"
+#include "base/strings/string_number_conversions.h"
+#include "components/webdata/encryptor/encryptor.h"
+#include "google_apis/gcm/base/mcs_util.h"
+#include "google_apis/gcm/engine/fake_connection_factory.h"
+#include "google_apis/gcm/engine/fake_connection_handler.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+namespace gcm {
+
+namespace {
+
+const uint64 kAndroidId = 54321;
+const uint64 kSecurityToken = 12345;
+
+// Number of messages to send when testing batching.
+// Note: must be even for tests that split batches in half.
+const int kMessageBatchSize = 6;
+
+// The number of unacked messages the client will receive before sending a
+// stream ack.
+// TODO(zea): get this (and other constants) directly from the mcs client.
+const int kAckLimitSize = 10;
+
+// Helper for building arbitrary data messages.
+MCSMessage BuildDataMessage(const std::string& from,
+ const std::string& category,
+ int last_stream_id_received,
+ const std::string persistent_id) {
+ mcs_proto::DataMessageStanza data_message;
+ data_message.set_from(from);
+ data_message.set_category(category);
+ data_message.set_last_stream_id_received(last_stream_id_received);
+ if (!persistent_id.empty())
+ data_message.set_persistent_id(persistent_id);
+ return MCSMessage(kDataMessageStanzaTag, data_message);
+}
+
+// MCSClient with overriden exposed persistent id logic.
+class TestMCSClient : public MCSClient {
+ public:
+ TestMCSClient(const base::FilePath& rmq_path,
+ ConnectionFactory* connection_factory,
+ scoped_refptr<base::SequencedTaskRunner> blocking_task_runner)
+ : MCSClient(rmq_path, connection_factory, blocking_task_runner),
+ next_id_(0) {
+ }
+
+ virtual std::string GetNextPersistentId() OVERRIDE {
+ return base::UintToString(++next_id_);
+ }
+
+ private:
+ uint32 next_id_;
+};
+
+class MCSClientTest : public testing::Test {
+ public:
+ MCSClientTest();
+ virtual ~MCSClientTest();
+
+ void BuildMCSClient();
+ void InitializeClient();
+ void LoginClient(const std::vector<std::string>& acknowledged_ids);
+
+ TestMCSClient* mcs_client() const { return mcs_client_.get(); }
+ FakeConnectionFactory* connection_factory() {
+ return &connection_factory_;
+ }
+ bool init_success() const { return init_success_; }
+ uint64 restored_android_id() const { return restored_android_id_; }
+ uint64 restored_security_token() const { return restored_security_token_; }
+ MCSMessage* received_message() const { return received_message_.get(); }
+ std::string sent_message_id() const { return sent_message_id_;}
+
+ FakeConnectionHandler* GetFakeHandler() const;
+
+ void WaitForMCSEvent();
+ void PumpLoop();
+
+ private:
+ void InitializationCallback(bool success,
+ uint64 restored_android_id,
+ uint64 restored_security_token);
+ void MessageReceivedCallback(const MCSMessage& message);
+ void MessageSentCallback(const std::string& message_id);
+
+ base::ScopedTempDir temp_directory_;
+ base::MessageLoop message_loop_;
+ scoped_ptr<base::RunLoop> run_loop_;
+
+ FakeConnectionFactory connection_factory_;
+ scoped_ptr<TestMCSClient> mcs_client_;
+ bool init_success_;
+ uint64 restored_android_id_;
+ uint64 restored_security_token_;
+ scoped_ptr<MCSMessage> received_message_;
+ std::string sent_message_id_;
+};
+
+MCSClientTest::MCSClientTest()
+ : run_loop_(new base::RunLoop()),
+ init_success_(false),
+ restored_android_id_(0),
+ restored_security_token_(0) {
+ EXPECT_TRUE(temp_directory_.CreateUniqueTempDir());
+ run_loop_.reset(new base::RunLoop());
+
+ // On OSX, prevent the Keychain permissions popup during unit tests.
+#if defined(OS_MACOSX)
+ Encryptor::UseMockKeychain(true);
+#endif
+}
+
+MCSClientTest::~MCSClientTest() {}
+
+void MCSClientTest::BuildMCSClient() {
+ mcs_client_.reset(
+ new TestMCSClient(temp_directory_.path(),
+ &connection_factory_,
+ message_loop_.message_loop_proxy()));
+}
+
+void MCSClientTest::InitializeClient() {
+ mcs_client_->Initialize(base::Bind(&MCSClientTest::InitializationCallback,
+ base::Unretained(this)),
+ base::Bind(&MCSClientTest::MessageReceivedCallback,
+ base::Unretained(this)),
+ base::Bind(&MCSClientTest::MessageSentCallback,
+ base::Unretained(this)));
+ run_loop_->Run();
+ run_loop_.reset(new base::RunLoop());
+}
+
+void MCSClientTest::LoginClient(
+ const std::vector<std::string>& acknowledged_ids) {
+ scoped_ptr<mcs_proto::LoginRequest> login_request =
+ BuildLoginRequest(kAndroidId, kSecurityToken);
+ for (size_t i = 0; i < acknowledged_ids.size(); ++i)
+ login_request->add_received_persistent_id(acknowledged_ids[i]);
+ GetFakeHandler()->ExpectOutgoingMessage(
+ MCSMessage(kLoginRequestTag,
+ login_request.PassAs<const google::protobuf::MessageLite>()));
+ mcs_client_->Login(kAndroidId, kSecurityToken);
+ run_loop_->Run();
+ run_loop_.reset(new base::RunLoop());
+}
+
+FakeConnectionHandler* MCSClientTest::GetFakeHandler() const {
+ return reinterpret_cast<FakeConnectionHandler*>(
+ connection_factory_.GetConnectionHandler());
+}
+
+void MCSClientTest::WaitForMCSEvent() {
+ run_loop_->Run();
+ run_loop_.reset(new base::RunLoop());
+}
+
+void MCSClientTest::PumpLoop() {
+ run_loop_->RunUntilIdle();
+ run_loop_.reset(new base::RunLoop());
+}
+
+void MCSClientTest::InitializationCallback(bool success,
+ uint64 restored_android_id,
+ uint64 restored_security_token) {
+ init_success_ = success;
+ restored_android_id_ = restored_android_id;
+ restored_security_token_ = restored_security_token;
+ DVLOG(1) << "Initialization callback invoked, killing loop.";
+ run_loop_->Quit();
+}
+
+void MCSClientTest::MessageReceivedCallback(const MCSMessage& message) {
+ received_message_.reset(new MCSMessage(message));
+ DVLOG(1) << "Message received callback invoked, killing loop.";
+ run_loop_->Quit();
+}
+
+void MCSClientTest::MessageSentCallback(const std::string& message_id) {
+ DVLOG(1) << "Message sent callback invoked, killing loop.";
+ run_loop_->Quit();
+}
+
+// Initialize a new client.
+TEST_F(MCSClientTest, InitializeNew) {
+ BuildMCSClient();
+ InitializeClient();
+ EXPECT_EQ(0U, restored_android_id());
+ EXPECT_EQ(0U, restored_security_token());
+ EXPECT_TRUE(init_success());
+}
+
+// Initialize a new client, shut it down, then restart the client. Should
+// reload the existing device credentials.
+TEST_F(MCSClientTest, InitializeExisting) {
+ BuildMCSClient();
+ InitializeClient();
+ LoginClient(std::vector<std::string>());
+
+ // Rebuild the client, to reload from the RMQ.
+ BuildMCSClient();
+ InitializeClient();
+ EXPECT_EQ(kAndroidId, restored_android_id());
+ EXPECT_EQ(kSecurityToken, restored_security_token());
+ EXPECT_TRUE(init_success());
+}
+
+// Log in successfully to the MCS endpoint.
+TEST_F(MCSClientTest, LoginSuccess) {
+ BuildMCSClient();
+ InitializeClient();
+ LoginClient(std::vector<std::string>());
+ EXPECT_TRUE(connection_factory()->IsEndpointReachable());
+ EXPECT_TRUE(init_success());
+ ASSERT_TRUE(received_message());
+ EXPECT_EQ(kLoginResponseTag, received_message()->tag());
+}
+
+// Encounter a server error during the login attempt.
+TEST_F(MCSClientTest, FailLogin) {
+ BuildMCSClient();
+ InitializeClient();
+ GetFakeHandler()->set_fail_login(true);
+ LoginClient(std::vector<std::string>());
+ EXPECT_FALSE(connection_factory()->IsEndpointReachable());
+ EXPECT_FALSE(init_success());
+ EXPECT_FALSE(received_message());
+}
+
+// Send a message without RMQ support.
+TEST_F(MCSClientTest, SendMessageNoRMQ) {
+ BuildMCSClient();
+ InitializeClient();
+ LoginClient(std::vector<std::string>());
+ MCSMessage message(BuildDataMessage("from", "category", 1, ""));
+ GetFakeHandler()->ExpectOutgoingMessage(message);
+ mcs_client()->SendMessage(message, false);
+ EXPECT_TRUE(GetFakeHandler()->
+ AllOutgoingMessagesReceived());
+}
+
+// Send a message with RMQ support.
+TEST_F(MCSClientTest, SendMessageRMQ) {
+ BuildMCSClient();
+ InitializeClient();
+ LoginClient(std::vector<std::string>());
+ MCSMessage message(BuildDataMessage("from", "category", 1, "1"));
+ GetFakeHandler()->ExpectOutgoingMessage(message);
+ mcs_client()->SendMessage(message, true);
+ EXPECT_TRUE(GetFakeHandler()->
+ AllOutgoingMessagesReceived());
+}
+
+// Send a message with RMQ support while disconnected. On reconnect, the message
+// should be resent.
+TEST_F(MCSClientTest, SendMessageRMQWhileDisconnected) {
+ BuildMCSClient();
+ InitializeClient();
+ LoginClient(std::vector<std::string>());
+ GetFakeHandler()->set_fail_send(true);
+ MCSMessage message(BuildDataMessage("from", "category", 1, "1"));
+
+ // The initial (failed) send.
+ GetFakeHandler()->ExpectOutgoingMessage(message);
+ // The login request.
+ GetFakeHandler()->ExpectOutgoingMessage(
+ MCSMessage(kLoginRequestTag,
+ BuildLoginRequest(kAndroidId, kSecurityToken).
+ PassAs<const google::protobuf::MessageLite>()));
+ // The second (re)send.
+ GetFakeHandler()->ExpectOutgoingMessage(message);
+ mcs_client()->SendMessage(message, true);
+ EXPECT_FALSE(GetFakeHandler()->
+ AllOutgoingMessagesReceived());
+ GetFakeHandler()->set_fail_send(false);
+ connection_factory()->Connect();
+ WaitForMCSEvent(); // Wait for the login to finish.
+ PumpLoop(); // Wait for the send to happen.
+ EXPECT_TRUE(GetFakeHandler()->
+ AllOutgoingMessagesReceived());
+}
+
+// Send a message with RMQ support without receiving an acknowledgement. On
+// restart the message should be resent.
+TEST_F(MCSClientTest, SendMessageRMQOnRestart) {
+ BuildMCSClient();
+ InitializeClient();
+ LoginClient(std::vector<std::string>());
+ GetFakeHandler()->set_fail_send(true);
+ MCSMessage message(BuildDataMessage("from", "category", 1, "1"));
+
+ // The initial (failed) send.
+ GetFakeHandler()->ExpectOutgoingMessage(message);
+ GetFakeHandler()->set_fail_send(false);
+ mcs_client()->SendMessage(message, true);
+ EXPECT_TRUE(GetFakeHandler()->
+ AllOutgoingMessagesReceived());
+
+ // Rebuild the client, which should resend the old message.
+ BuildMCSClient();
+ InitializeClient();
+ LoginClient(std::vector<std::string>());
+ GetFakeHandler()->ExpectOutgoingMessage(message);
+ PumpLoop();
+ EXPECT_TRUE(GetFakeHandler()->
+ AllOutgoingMessagesReceived());
+}
+
+// Send messages with RMQ support, followed by receiving a stream ack. On
+// restart nothing should be recent.
+TEST_F(MCSClientTest, SendMessageRMQWithStreamAck) {
+ BuildMCSClient();
+ InitializeClient();
+ LoginClient(std::vector<std::string>());
+
+ // Send some messages.
+ for (int i = 1; i <= kMessageBatchSize; ++i) {
+ MCSMessage message(
+ BuildDataMessage("from", "category", 1, base::IntToString(i)));
+ GetFakeHandler()->ExpectOutgoingMessage(message);
+ mcs_client()->SendMessage(message, true);
+ }
+ EXPECT_TRUE(GetFakeHandler()->
+ AllOutgoingMessagesReceived());
+
+ // Receive the ack.
+ scoped_ptr<mcs_proto::IqStanza> ack = BuildStreamAck();
+ ack->set_last_stream_id_received(kMessageBatchSize + 1);
+ GetFakeHandler()->ReceiveMessage(
+ MCSMessage(kIqStanzaTag,
+ ack.PassAs<const google::protobuf::MessageLite>()));
+ WaitForMCSEvent();
+
+ // Reconnect and ensure no messages are resent.
+ BuildMCSClient();
+ InitializeClient();
+ LoginClient(std::vector<std::string>());
+ PumpLoop();
+}
+
+// Send messages with RMQ support. On restart, receive a SelectiveAck with
+// the login response. No messages should be resent.
+TEST_F(MCSClientTest, SendMessageRMQAckOnReconnect) {
+ BuildMCSClient();
+ InitializeClient();
+ LoginClient(std::vector<std::string>());
+
+ // Send some messages.
+ std::vector<std::string> id_list;
+ for (int i = 1; i <= kMessageBatchSize; ++i) {
+ id_list.push_back(base::IntToString(i));
+ MCSMessage message(
+ BuildDataMessage("from", "category", 1, id_list.back()));
+ GetFakeHandler()->ExpectOutgoingMessage(message);
+ mcs_client()->SendMessage(message, true);
+ }
+ EXPECT_TRUE(GetFakeHandler()->
+ AllOutgoingMessagesReceived());
+
+ // Rebuild the client, and receive an acknowledgment for the messages as
+ // part of the login response.
+ BuildMCSClient();
+ InitializeClient();
+ LoginClient(std::vector<std::string>());
+ scoped_ptr<mcs_proto::IqStanza> ack(BuildSelectiveAck(id_list));
+ GetFakeHandler()->ReceiveMessage(
+ MCSMessage(kIqStanzaTag,
+ ack.PassAs<const google::protobuf::MessageLite>()));
+ WaitForMCSEvent();
+ EXPECT_TRUE(GetFakeHandler()->
+ AllOutgoingMessagesReceived());
+}
+
+// Send messages with RMQ support. On restart, receive a SelectiveAck with
+// the login response that only acks some messages. The unacked messages should
+// be resent.
+TEST_F(MCSClientTest, SendMessageRMQPartialAckOnReconnect) {
+ BuildMCSClient();
+ InitializeClient();
+ LoginClient(std::vector<std::string>());
+
+ // Send some messages.
+ std::vector<std::string> id_list;
+ for (int i = 1; i <= kMessageBatchSize; ++i) {
+ id_list.push_back(base::IntToString(i));
+ MCSMessage message(
+ BuildDataMessage("from", "category", 1, id_list.back()));
+ GetFakeHandler()->ExpectOutgoingMessage(message);
+ mcs_client()->SendMessage(message, true);
+ }
+ EXPECT_TRUE(GetFakeHandler()->
+ AllOutgoingMessagesReceived());
+
+ // Rebuild the client, and receive an acknowledgment for the messages as
+ // part of the login response.
+ BuildMCSClient();
+ InitializeClient();
+ LoginClient(std::vector<std::string>());
+
+ std::vector<std::string> acked_ids, remaining_ids;
+ acked_ids.insert(acked_ids.end(),
+ id_list.begin(),
+ id_list.begin() + kMessageBatchSize / 2);
+ remaining_ids.insert(remaining_ids.end(),
+ id_list.begin() + kMessageBatchSize / 2,
+ id_list.end());
+ for (int i = 1; i <= kMessageBatchSize / 2; ++i) {
+ MCSMessage message(
+ BuildDataMessage("from",
+ "category",
+ 2,
+ remaining_ids[i - 1]));
+ GetFakeHandler()->ExpectOutgoingMessage(message);
+ }
+ scoped_ptr<mcs_proto::IqStanza> ack(BuildSelectiveAck(acked_ids));
+ GetFakeHandler()->ReceiveMessage(
+ MCSMessage(kIqStanzaTag,
+ ack.PassAs<const google::protobuf::MessageLite>()));
+ WaitForMCSEvent();
+ EXPECT_TRUE(GetFakeHandler()->
+ AllOutgoingMessagesReceived());
+}
+
+// Receive some messages. On restart, the login request should contain the
+// appropriate acknowledged ids.
+TEST_F(MCSClientTest, AckOnLogin) {
+ BuildMCSClient();
+ InitializeClient();
+ LoginClient(std::vector<std::string>());
+
+ // Receive some messages.
+ std::vector<std::string> id_list;
+ for (int i = 1; i <= kMessageBatchSize; ++i) {
+ id_list.push_back(base::IntToString(i));
+ MCSMessage message(
+ BuildDataMessage("from", "category", i, id_list.back()));
+ GetFakeHandler()->ReceiveMessage(message);
+ WaitForMCSEvent();
+ PumpLoop();
+ }
+
+ // Restart the client.
+ BuildMCSClient();
+ InitializeClient();
+ LoginClient(id_list);
+}
+
+// Receive some messages. On the next send, the outgoing message should contain
+// the appropriate last stream id received field to ack the received messages.
+TEST_F(MCSClientTest, AckOnSend) {
+ BuildMCSClient();
+ InitializeClient();
+ LoginClient(std::vector<std::string>());
+
+ // Receive some messages.
+ std::vector<std::string> id_list;
+ for (int i = 1; i <= kMessageBatchSize; ++i) {
+ id_list.push_back(base::IntToString(i));
+ MCSMessage message(
+ BuildDataMessage("from", "category", i, id_list.back()));
+ GetFakeHandler()->ReceiveMessage(message);
+ WaitForMCSEvent();
+ PumpLoop();
+ }
+
+ // Trigger a message send, which should acknowledge via stream ack.
+ MCSMessage message(
+ BuildDataMessage("from", "category", kMessageBatchSize + 1, "1"));
+ GetFakeHandler()->ExpectOutgoingMessage(message);
+ mcs_client()->SendMessage(message, true);
+ EXPECT_TRUE(GetFakeHandler()->
+ AllOutgoingMessagesReceived());
+}
+
+// Receive the ack limit in messages, which should trigger an automatic
+// stream ack. Receive a heartbeat to confirm the ack.
+TEST_F(MCSClientTest, AckWhenLimitReachedWithHeartbeat) {
+ BuildMCSClient();
+ InitializeClient();
+ LoginClient(std::vector<std::string>());
+
+ // The stream ack.
+ scoped_ptr<mcs_proto::IqStanza> ack = BuildStreamAck();
+ ack->set_last_stream_id_received(kAckLimitSize + 1);
+ GetFakeHandler()->ExpectOutgoingMessage(
+ MCSMessage(kIqStanzaTag,
+ ack.PassAs<const google::protobuf::MessageLite>()));
+
+ // Receive some messages.
+ std::vector<std::string> id_list;
+ for (int i = 1; i <= kAckLimitSize; ++i) {
+ id_list.push_back(base::IntToString(i));
+ MCSMessage message(
+ BuildDataMessage("from", "category", i, id_list.back()));
+ GetFakeHandler()->ReceiveMessage(message);
+ WaitForMCSEvent();
+ PumpLoop();
+ }
+ EXPECT_TRUE(GetFakeHandler()->
+ AllOutgoingMessagesReceived());
+
+ // Receive a heartbeat confirming the ack (and receive the heartbeat ack).
+ scoped_ptr<mcs_proto::HeartbeatPing> heartbeat(
+ new mcs_proto::HeartbeatPing());
+ heartbeat->set_last_stream_id_received(2);
+
+ scoped_ptr<mcs_proto::HeartbeatAck> heartbeat_ack(
+ new mcs_proto::HeartbeatAck());
+ heartbeat_ack->set_last_stream_id_received(kAckLimitSize + 2);
+ GetFakeHandler()->ExpectOutgoingMessage(
+ MCSMessage(kHeartbeatAckTag,
+ heartbeat_ack.PassAs<const google::protobuf::MessageLite>()));
+
+ GetFakeHandler()->ReceiveMessage(
+ MCSMessage(kHeartbeatPingTag,
+ heartbeat.PassAs<const google::protobuf::MessageLite>()));
+ WaitForMCSEvent();
+ EXPECT_TRUE(GetFakeHandler()->
+ AllOutgoingMessagesReceived());
+
+ // Rebuild the client. Nothing should be sent on login.
+ BuildMCSClient();
+ InitializeClient();
+ LoginClient(std::vector<std::string>());
+ EXPECT_TRUE(GetFakeHandler()->
+ AllOutgoingMessagesReceived());
+}
+
+} // namespace
+
+} // namespace gcm
diff --git a/chromium/google_apis/gcm/engine/rmq_store.cc b/chromium/google_apis/gcm/engine/rmq_store.cc
new file mode 100644
index 00000000000..eaa6dcc8059
--- /dev/null
+++ b/chromium/google_apis/gcm/engine/rmq_store.cc
@@ -0,0 +1,491 @@
+// Copyright 2013 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "google_apis/gcm/engine/rmq_store.h"
+
+#include "base/basictypes.h"
+#include "base/bind.h"
+#include "base/callback.h"
+#include "base/files/file_path.h"
+#include "base/logging.h"
+#include "base/message_loop/message_loop_proxy.h"
+#include "base/sequenced_task_runner.h"
+#include "base/stl_util.h"
+#include "base/strings/string_number_conversions.h"
+#include "base/strings/string_piece.h"
+#include "base/tracked_objects.h"
+#include "components/webdata/encryptor/encryptor.h"
+#include "google_apis/gcm/base/mcs_message.h"
+#include "google_apis/gcm/base/mcs_util.h"
+#include "google_apis/gcm/protocol/mcs.pb.h"
+#include "third_party/leveldatabase/src/include/leveldb/db.h"
+
+namespace gcm {
+
+namespace {
+
+// ---- LevelDB keys. ----
+// Key for this device's android id.
+const char kDeviceAIDKey[] = "device_aid_key";
+// Key for this device's android security token.
+const char kDeviceTokenKey[] = "device_token_key";
+// Lowest lexicographically ordered incoming message key.
+// Used for prefixing messages.
+const char kIncomingMsgKeyStart[] = "incoming1-";
+// Key guaranteed to be higher than all incoming message keys.
+// Used for limiting iteration.
+const char kIncomingMsgKeyEnd[] = "incoming2-";
+// Lowest lexicographically ordered outgoing message key.
+// Used for prefixing outgoing messages.
+const char kOutgoingMsgKeyStart[] = "outgoing1-";
+// Key guaranteed to be higher than all outgoing message keys.
+// Used for limiting iteration.
+const char kOutgoingMsgKeyEnd[] = "outgoing2-";
+
+std::string MakeIncomingKey(const std::string& persistent_id) {
+ return kIncomingMsgKeyStart + persistent_id;
+}
+
+std::string MakeOutgoingKey(const std::string& persistent_id) {
+ return kOutgoingMsgKeyStart + persistent_id;
+}
+
+std::string ParseOutgoingKey(const std::string& key) {
+ return key.substr(arraysize(kOutgoingMsgKeyStart) - 1);
+}
+
+leveldb::Slice MakeSlice(const base::StringPiece& s) {
+ return leveldb::Slice(s.begin(), s.size());
+}
+
+} // namespace
+
+class RMQStore::Backend : public base::RefCountedThreadSafe<RMQStore::Backend> {
+ public:
+ Backend(const base::FilePath& path,
+ scoped_refptr<base::SequencedTaskRunner> foreground_runner);
+
+ // Blocking implementations of RMQStore methods.
+ void Load(const LoadCallback& callback);
+ void Destroy(const UpdateCallback& callback);
+ void SetDeviceCredentials(uint64 device_android_id,
+ uint64 device_security_token,
+ const UpdateCallback& callback);
+ void AddIncomingMessage(const std::string& persistent_id,
+ const UpdateCallback& callback);
+ void RemoveIncomingMessages(const PersistentIdList& persistent_ids,
+ const UpdateCallback& callback);
+ void AddOutgoingMessage(const std::string& persistent_id,
+ const MCSMessage& message,
+ const UpdateCallback& callback);
+ void RemoveOutgoingMessages(const PersistentIdList& persistent_ids,
+ const UpdateCallback& callback);
+
+ private:
+ friend class base::RefCountedThreadSafe<Backend>;
+ ~Backend();
+
+ bool LoadDeviceCredentials(uint64* android_id, uint64* security_token);
+ bool LoadIncomingMessages(std::vector<std::string>* incoming_messages);
+ bool LoadOutgoingMessages(
+ std::map<std::string, google::protobuf::MessageLite*>* outgoing_messages);
+
+ const base::FilePath path_;
+ scoped_refptr<base::SequencedTaskRunner> foreground_task_runner_;
+
+ scoped_ptr<leveldb::DB> db_;
+};
+
+RMQStore::Backend::Backend(
+ const base::FilePath& path,
+ scoped_refptr<base::SequencedTaskRunner> foreground_task_runner)
+ : path_(path),
+ foreground_task_runner_(foreground_task_runner) {
+}
+
+RMQStore::Backend::~Backend() {
+}
+
+void RMQStore::Backend::Load(const LoadCallback& callback) {
+ LoadResult result;
+
+ leveldb::Options options;
+ options.create_if_missing = true;
+ leveldb::DB* db;
+ leveldb::Status status = leveldb::DB::Open(options,
+ path_.AsUTF8Unsafe(),
+ &db);
+ if (!status.ok()) {
+ LOG(ERROR) << "Failed to open database " << path_.value()
+ << ": " << status.ToString();
+ foreground_task_runner_->PostTask(FROM_HERE,
+ base::Bind(callback, result));
+ return;
+ }
+ db_.reset(db);
+
+ if (!LoadDeviceCredentials(&result.device_android_id,
+ &result.device_security_token) ||
+ !LoadIncomingMessages(&result.incoming_messages) ||
+ !LoadOutgoingMessages(&result.outgoing_messages)) {
+ result.device_android_id = 0;
+ result.device_security_token = 0;
+ result.incoming_messages.clear();
+ STLDeleteContainerPairSecondPointers(result.outgoing_messages.begin(),
+ result.outgoing_messages.end());
+ result.outgoing_messages.clear();
+ foreground_task_runner_->PostTask(FROM_HERE,
+ base::Bind(callback, result));
+ return;
+ }
+
+ DVLOG(1) << "Succeeded in loading " << result.incoming_messages.size()
+ << " unacknowledged incoming messages and "
+ << result.outgoing_messages.size()
+ << " unacknowledged outgoing messages.";
+ result.success = true;
+ foreground_task_runner_->PostTask(FROM_HERE,
+ base::Bind(callback, result));
+ return;
+}
+
+void RMQStore::Backend::Destroy(const UpdateCallback& callback) {
+ DVLOG(1) << "Destroying RMQ store.";
+ const leveldb::Status s =
+ leveldb::DestroyDB(path_.AsUTF8Unsafe(),
+ leveldb::Options());
+ if (s.ok()) {
+ foreground_task_runner_->PostTask(FROM_HERE,
+ base::Bind(callback, true));
+ return;
+ }
+ LOG(ERROR) << "Destroy failed.";
+ foreground_task_runner_->PostTask(FROM_HERE,
+ base::Bind(callback, false));
+}
+
+void RMQStore::Backend::SetDeviceCredentials(uint64 device_android_id,
+ uint64 device_security_token,
+ const UpdateCallback& callback) {
+ DVLOG(1) << "Saving device credentials with AID " << device_android_id;
+ leveldb::WriteOptions write_options;
+ write_options.sync = true;
+
+ std::string encrypted_token;
+ Encryptor::EncryptString(base::Uint64ToString(device_security_token),
+ &encrypted_token);
+ leveldb::Status s =
+ db_->Put(write_options,
+ MakeSlice(kDeviceAIDKey),
+ MakeSlice(base::Uint64ToString(device_android_id)));
+ if (s.ok()) {
+ s = db_->Put(write_options,
+ MakeSlice(kDeviceTokenKey),
+ MakeSlice(encrypted_token));
+ }
+ if (s.ok()) {
+ foreground_task_runner_->PostTask(FROM_HERE,
+ base::Bind(callback, true));
+ return;
+ }
+ LOG(ERROR) << "LevelDB put failed: " << s.ToString();
+ foreground_task_runner_->PostTask(FROM_HERE,
+ base::Bind(callback, false));
+}
+
+void RMQStore::Backend::AddIncomingMessage(const std::string& persistent_id,
+ const UpdateCallback& callback) {
+ DVLOG(1) << "Saving incoming message with id " << persistent_id;
+ leveldb::WriteOptions write_options;
+ write_options.sync = true;
+
+ const leveldb::Status s =
+ db_->Put(write_options,
+ MakeSlice(MakeIncomingKey(persistent_id)),
+ MakeSlice(persistent_id));
+ if (s.ok()) {
+ foreground_task_runner_->PostTask(FROM_HERE,
+ base::Bind(callback, true));
+ return;
+ }
+ LOG(ERROR) << "LevelDB put failed: " << s.ToString();
+ foreground_task_runner_->PostTask(FROM_HERE,
+ base::Bind(callback, false));
+}
+
+void RMQStore::Backend::RemoveIncomingMessages(
+ const PersistentIdList& persistent_ids,
+ const UpdateCallback& callback) {
+ leveldb::WriteOptions write_options;
+ write_options.sync = true;
+
+ leveldb::Status s;
+ for (PersistentIdList::const_iterator iter = persistent_ids.begin();
+ iter != persistent_ids.end(); ++iter){
+ DVLOG(1) << "Removing incoming message with id " << *iter;
+ s = db_->Delete(write_options,
+ MakeSlice(MakeIncomingKey(*iter)));
+ if (!s.ok())
+ break;
+ }
+ if (s.ok()) {
+ foreground_task_runner_->PostTask(FROM_HERE,
+ base::Bind(callback, true));
+ return;
+ }
+ LOG(ERROR) << "LevelDB remove failed: " << s.ToString();
+ foreground_task_runner_->PostTask(FROM_HERE,
+ base::Bind(callback, false));
+}
+
+void RMQStore::Backend::AddOutgoingMessage(
+ const std::string& persistent_id,
+ const MCSMessage& message,
+ const UpdateCallback& callback) {
+ DVLOG(1) << "Saving outgoing message with id " << persistent_id;
+ leveldb::WriteOptions write_options;
+ write_options.sync = true;
+
+ std::string data = static_cast<char>(message.tag()) +
+ message.SerializeAsString();
+ const leveldb::Status s =
+ db_->Put(write_options,
+ MakeSlice(MakeOutgoingKey(persistent_id)),
+ MakeSlice(data));
+ if (s.ok()) {
+ foreground_task_runner_->PostTask(FROM_HERE,
+ base::Bind(callback, true));
+ return;
+ }
+ LOG(ERROR) << "LevelDB put failed: " << s.ToString();
+ foreground_task_runner_->PostTask(FROM_HERE,
+ base::Bind(callback, false));
+
+}
+
+void RMQStore::Backend::RemoveOutgoingMessages(
+ const PersistentIdList& persistent_ids,
+ const UpdateCallback& callback) {
+ leveldb::WriteOptions write_options;
+ write_options.sync = true;
+
+ leveldb::Status s;
+ for (PersistentIdList::const_iterator iter = persistent_ids.begin();
+ iter != persistent_ids.end(); ++iter){
+ DVLOG(1) << "Removing outgoing message with id " << *iter;
+ s = db_->Delete(write_options,
+ MakeSlice(MakeOutgoingKey(*iter)));
+ if (!s.ok())
+ break;
+ }
+ if (s.ok()) {
+ foreground_task_runner_->PostTask(FROM_HERE,
+ base::Bind(callback, true));
+ return;
+ }
+ LOG(ERROR) << "LevelDB remove failed: " << s.ToString();
+ foreground_task_runner_->PostTask(FROM_HERE,
+ base::Bind(callback, false));
+}
+
+bool RMQStore::Backend::LoadDeviceCredentials(uint64* android_id,
+ uint64* security_token) {
+ leveldb::ReadOptions read_options;
+ read_options.verify_checksums = true;
+
+ std::string result;
+ leveldb::Status s = db_->Get(read_options,
+ MakeSlice(kDeviceAIDKey),
+ &result);
+ if (s.ok()) {
+ if (!base::StringToUint64(result, android_id)) {
+ LOG(ERROR) << "Failed to restore device id.";
+ return false;
+ }
+ result.clear();
+ s = db_->Get(read_options,
+ MakeSlice(kDeviceTokenKey),
+ &result);
+ }
+ if (s.ok()) {
+ std::string decrypted_token;
+ Encryptor::DecryptString(result, &decrypted_token);
+ if (!base::StringToUint64(decrypted_token, security_token)) {
+ LOG(ERROR) << "Failed to restore security token.";
+ return false;
+ }
+ return true;
+ }
+
+ if (s.IsNotFound()) {
+ DVLOG(1) << "No credentials found.";
+ return true;
+ }
+
+ LOG(ERROR) << "Error reading credentials from store.";
+ return false;
+}
+
+bool RMQStore::Backend::LoadIncomingMessages(
+ std::vector<std::string>* incoming_messages) {
+ leveldb::ReadOptions read_options;
+ read_options.verify_checksums = true;
+
+ scoped_ptr<leveldb::Iterator> iter(db_->NewIterator(read_options));
+ for (iter->Seek(MakeSlice(kIncomingMsgKeyStart));
+ iter->Valid() && iter->key().ToString() < kIncomingMsgKeyEnd;
+ iter->Next()) {
+ leveldb::Slice s = iter->value();
+ if (s.empty()) {
+ LOG(ERROR) << "Error reading incoming message with key "
+ << iter->key().ToString();
+ return false;
+ }
+ DVLOG(1) << "Found incoming message with id " << s.ToString();
+ incoming_messages->push_back(s.ToString());
+ }
+
+ return true;
+}
+
+bool RMQStore::Backend::LoadOutgoingMessages(
+ std::map<std::string, google::protobuf::MessageLite*>*
+ outgoing_messages) {
+ leveldb::ReadOptions read_options;
+ read_options.verify_checksums = true;
+
+ scoped_ptr<leveldb::Iterator> iter(db_->NewIterator(read_options));
+ for (iter->Seek(MakeSlice(kOutgoingMsgKeyStart));
+ iter->Valid() && iter->key().ToString() < kOutgoingMsgKeyEnd;
+ iter->Next()) {
+ leveldb::Slice s = iter->value();
+ if (s.size() <= 1) {
+ LOG(ERROR) << "Error reading incoming message with key " << s.ToString();
+ return false;
+ }
+ uint8 tag = iter->value().data()[0];
+ std::string id = ParseOutgoingKey(iter->key().ToString());
+ scoped_ptr<google::protobuf::MessageLite> message(
+ BuildProtobufFromTag(tag));
+ if (!message.get() ||
+ !message->ParseFromString(iter->value().ToString().substr(1))) {
+ LOG(ERROR) << "Failed to parse outgoing message with id "
+ << id << " and tag " << tag;
+ return false;
+ }
+ DVLOG(1) << "Found outgoing message with id " << id << " of type "
+ << base::IntToString(tag);
+ (*outgoing_messages)[id] = message.release();
+ }
+
+ return true;
+}
+
+RMQStore::LoadResult::LoadResult()
+ : success(false),
+ device_android_id(0),
+ device_security_token(0) {
+}
+RMQStore::LoadResult::~LoadResult() {}
+
+RMQStore::RMQStore(
+ const base::FilePath& path,
+ scoped_refptr<base::SequencedTaskRunner> blocking_task_runner)
+ : backend_(new Backend(path, base::MessageLoopProxy::current())),
+ blocking_task_runner_(blocking_task_runner) {
+}
+
+RMQStore::~RMQStore() {
+}
+
+void RMQStore::Load(const LoadCallback& callback) {
+ blocking_task_runner_->PostTask(FROM_HERE,
+ base::Bind(&RMQStore::Backend::Load,
+ backend_,
+ callback));
+}
+
+void RMQStore::Destroy(const UpdateCallback& callback) {
+ blocking_task_runner_->PostTask(
+ FROM_HERE,
+ base::Bind(&RMQStore::Backend::Destroy,
+ backend_,
+ callback));
+}
+
+void RMQStore::SetDeviceCredentials(uint64 device_android_id,
+ uint64 device_security_token,
+ const UpdateCallback& callback) {
+ blocking_task_runner_->PostTask(
+ FROM_HERE,
+ base::Bind(&RMQStore::Backend::SetDeviceCredentials,
+ backend_,
+ device_android_id,
+ device_security_token,
+ callback));
+}
+
+void RMQStore::AddIncomingMessage(const std::string& persistent_id,
+ const UpdateCallback& callback) {
+ blocking_task_runner_->PostTask(
+ FROM_HERE,
+ base::Bind(&RMQStore::Backend::AddIncomingMessage,
+ backend_,
+ persistent_id,
+ callback));
+}
+
+void RMQStore::RemoveIncomingMessage(const std::string& persistent_id,
+ const UpdateCallback& callback) {
+ blocking_task_runner_->PostTask(
+ FROM_HERE,
+ base::Bind(&RMQStore::Backend::RemoveIncomingMessages,
+ backend_,
+ PersistentIdList(1, persistent_id),
+ callback));
+}
+
+void RMQStore::RemoveIncomingMessages(const PersistentIdList& persistent_ids,
+ const UpdateCallback& callback) {
+ blocking_task_runner_->PostTask(
+ FROM_HERE,
+ base::Bind(&RMQStore::Backend::RemoveIncomingMessages,
+ backend_,
+ persistent_ids,
+ callback));
+}
+
+void RMQStore::AddOutgoingMessage(const std::string& persistent_id,
+ const MCSMessage& message,
+ const UpdateCallback& callback) {
+ blocking_task_runner_->PostTask(
+ FROM_HERE,
+ base::Bind(&RMQStore::Backend::AddOutgoingMessage,
+ backend_,
+ persistent_id,
+ message,
+ callback));
+}
+
+void RMQStore::RemoveOutgoingMessage(const std::string& persistent_id,
+ const UpdateCallback& callback) {
+ blocking_task_runner_->PostTask(
+ FROM_HERE,
+ base::Bind(&RMQStore::Backend::RemoveOutgoingMessages,
+ backend_,
+ PersistentIdList(1, persistent_id),
+ callback));
+}
+
+void RMQStore::RemoveOutgoingMessages(const PersistentIdList& persistent_ids,
+ const UpdateCallback& callback) {
+ blocking_task_runner_->PostTask(
+ FROM_HERE,
+ base::Bind(&RMQStore::Backend::RemoveOutgoingMessages,
+ backend_,
+ persistent_ids,
+ callback));
+}
+
+} // namespace gcm
diff --git a/chromium/google_apis/gcm/engine/rmq_store.h b/chromium/google_apis/gcm/engine/rmq_store.h
new file mode 100644
index 00000000000..d3762a199c7
--- /dev/null
+++ b/chromium/google_apis/gcm/engine/rmq_store.h
@@ -0,0 +1,102 @@
+// Copyright 2013 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef GOOGLE_APIS_GCM_ENGINE_RMQ_STORE_H_
+#define GOOGLE_APIS_GCM_ENGINE_RMQ_STORE_H_
+
+#include <map>
+#include <string>
+#include <vector>
+
+#include "base/basictypes.h"
+#include "base/callback_forward.h"
+#include "base/memory/ref_counted.h"
+#include "google_apis/gcm/base/gcm_export.h"
+
+namespace base {
+class FilePath;
+class SequencedTaskRunner;
+} // namespace base
+
+namespace google {
+namespace protobuf {
+class MessageLite;
+} // namespace protobuf
+} // namespace google
+
+namespace gcm {
+
+class MCSMessage;
+
+// A Reliable Message Queue store.
+// Will perform all blocking operations on the blocking task runner, and will
+// post all callbacks to the thread on which the RMQStore is created.
+class GCM_EXPORT RMQStore {
+ public:
+ // Container for Load(..) results.
+ struct GCM_EXPORT LoadResult {
+ LoadResult();
+ ~LoadResult();
+
+ bool success;
+ uint64 device_android_id;
+ uint64 device_security_token;
+ std::vector<std::string> incoming_messages;
+ std::map<std::string, google::protobuf::MessageLite*>
+ outgoing_messages;
+ };
+
+ typedef std::vector<std::string> PersistentIdList;
+ // Note: callee receives ownership of |outgoing_messages|' values.
+ typedef base::Callback<void(const LoadResult& result)> LoadCallback;
+ typedef base::Callback<void(bool success)> UpdateCallback;
+
+ RMQStore(const base::FilePath& path,
+ scoped_refptr<base::SequencedTaskRunner> blocking_task_runner);
+ ~RMQStore();
+
+ // Load the directory and pass the initial state back to caller.
+ void Load(const LoadCallback& callback);
+
+ // Clears the RMQ store of all data and destroys any LevelDB files associated
+ // with this store.
+ // WARNING: this will permanently destroy any pending outgoing messages
+ // and require the device to re-create credentials.
+ void Destroy(const UpdateCallback& callback);
+
+ // Sets this device's messaging credentials.
+ void SetDeviceCredentials(uint64 device_android_id,
+ uint64 device_security_token,
+ const UpdateCallback& callback);
+
+ // Unacknowledged incoming message handling.
+ void AddIncomingMessage(const std::string& persistent_id,
+ const UpdateCallback& callback);
+ void RemoveIncomingMessage(const std::string& persistent_id,
+ const UpdateCallback& callback);
+ void RemoveIncomingMessages(const PersistentIdList& persistent_ids,
+ const UpdateCallback& callback);
+
+ // Unacknowledged outgoing messages handling.
+ // TODO(zea): implement per-app limits on the number of outgoing messages.
+ void AddOutgoingMessage(const std::string& persistent_id,
+ const MCSMessage& message,
+ const UpdateCallback& callback);
+ void RemoveOutgoingMessage(const std::string& persistent_id,
+ const UpdateCallback& callback);
+ void RemoveOutgoingMessages(const PersistentIdList& persistent_ids,
+ const UpdateCallback& callback);
+
+ private:
+ class Backend;
+
+ scoped_refptr<Backend> backend_;
+ scoped_refptr<base::SequencedTaskRunner> blocking_task_runner_;
+
+ DISALLOW_COPY_AND_ASSIGN(RMQStore);
+};
+
+} // namespace gcm
+
+#endif // GOOGLE_APIS_GCM_ENGINE_RMQ_STORE_H_
diff --git a/chromium/google_apis/gcm/engine/rmq_store_unittest.cc b/chromium/google_apis/gcm/engine/rmq_store_unittest.cc
new file mode 100644
index 00000000000..1fd55bcf14b
--- /dev/null
+++ b/chromium/google_apis/gcm/engine/rmq_store_unittest.cc
@@ -0,0 +1,303 @@
+// Copyright 2013 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "google_apis/gcm/engine/rmq_store.h"
+
+#include <string>
+#include <vector>
+
+#include "base/bind.h"
+#include "base/files/file_path.h"
+#include "base/files/scoped_temp_dir.h"
+#include "base/memory/scoped_ptr.h"
+#include "base/message_loop/message_loop.h"
+#include "base/run_loop.h"
+#include "base/strings/string_number_conversions.h"
+#include "components/webdata/encryptor/encryptor.h"
+#include "google_apis/gcm/base/mcs_message.h"
+#include "google_apis/gcm/base/mcs_util.h"
+#include "google_apis/gcm/protocol/mcs.pb.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+namespace gcm {
+
+namespace {
+
+// Number of persistent ids to use in tests.
+const int kNumPersistentIds = 10;
+
+const uint64 kDeviceId = 22;
+const uint64 kDeviceToken = 55;
+
+class RMQStoreTest : public testing::Test {
+ public:
+ RMQStoreTest();
+ virtual ~RMQStoreTest();
+
+ scoped_ptr<RMQStore> BuildRMQStore();
+
+ std::string GetNextPersistentId();
+
+ void PumpLoop();
+
+ void LoadCallback(RMQStore::LoadResult* result_dst,
+ const RMQStore::LoadResult& result);
+ void UpdateCallback(bool success);
+
+ private:
+ base::MessageLoop message_loop_;
+ base::ScopedTempDir temp_directory_;
+ scoped_ptr<base::RunLoop> run_loop_;
+};
+
+RMQStoreTest::RMQStoreTest() {
+ EXPECT_TRUE(temp_directory_.CreateUniqueTempDir());
+ run_loop_.reset(new base::RunLoop());
+
+ // On OSX, prevent the Keychain permissions popup during unit tests.
+ #if defined(OS_MACOSX)
+ Encryptor::UseMockKeychain(true);
+ #endif
+}
+
+RMQStoreTest::~RMQStoreTest() {
+}
+
+scoped_ptr<RMQStore> RMQStoreTest::BuildRMQStore() {
+ return scoped_ptr<RMQStore>(new RMQStore(temp_directory_.path(),
+ message_loop_.message_loop_proxy()));
+}
+
+std::string RMQStoreTest::GetNextPersistentId() {
+ return base::Uint64ToString(base::Time::Now().ToInternalValue());
+}
+
+void RMQStoreTest::PumpLoop() {
+ message_loop_.RunUntilIdle();
+}
+
+void RMQStoreTest::LoadCallback(RMQStore::LoadResult* result_dst,
+ const RMQStore::LoadResult& result) {
+ ASSERT_TRUE(result.success);
+ *result_dst = result;
+ run_loop_->Quit();
+ run_loop_.reset(new base::RunLoop());
+}
+
+void RMQStoreTest::UpdateCallback(bool success) {
+ ASSERT_TRUE(success);
+}
+
+// Verify creating a new database and loading it.
+TEST_F(RMQStoreTest, LoadNew) {
+ scoped_ptr<RMQStore> rmq_store(BuildRMQStore());
+ RMQStore::LoadResult load_result;
+ rmq_store->Load(base::Bind(&RMQStoreTest::LoadCallback,
+ base::Unretained(this),
+ &load_result));
+ PumpLoop();
+
+ ASSERT_EQ(0U, load_result.device_android_id);
+ ASSERT_EQ(0U, load_result.device_security_token);
+ ASSERT_TRUE(load_result.incoming_messages.empty());
+ ASSERT_TRUE(load_result.outgoing_messages.empty());
+}
+
+TEST_F(RMQStoreTest, DeviceCredentials) {
+ scoped_ptr<RMQStore> rmq_store(BuildRMQStore());
+ RMQStore::LoadResult load_result;
+ rmq_store->Load(base::Bind(&RMQStoreTest::LoadCallback,
+ base::Unretained(this),
+ &load_result));
+ PumpLoop();
+
+ rmq_store->SetDeviceCredentials(kDeviceId,
+ kDeviceToken,
+ base::Bind(&RMQStoreTest::UpdateCallback,
+ base::Unretained(this)));
+ PumpLoop();
+
+ rmq_store = BuildRMQStore().Pass();
+ rmq_store->Load(base::Bind(&RMQStoreTest::LoadCallback,
+ base::Unretained(this),
+ &load_result));
+ PumpLoop();
+
+ ASSERT_EQ(kDeviceId, load_result.device_android_id);
+ ASSERT_EQ(kDeviceToken, load_result.device_security_token);
+}
+
+// Verify saving some incoming messages, reopening the directory, and then
+// removing those incoming messages.
+TEST_F(RMQStoreTest, IncomingMessages) {
+ scoped_ptr<RMQStore> rmq_store(BuildRMQStore());
+ RMQStore::LoadResult load_result;
+ rmq_store->Load(base::Bind(&RMQStoreTest::LoadCallback,
+ base::Unretained(this),
+ &load_result));
+ PumpLoop();
+
+ std::vector<std::string> persistent_ids;
+ for (int i = 0; i < kNumPersistentIds; ++i) {
+ persistent_ids.push_back(GetNextPersistentId());
+ rmq_store->AddIncomingMessage(persistent_ids.back(),
+ base::Bind(&RMQStoreTest::UpdateCallback,
+ base::Unretained(this)));
+ PumpLoop();
+ }
+
+ rmq_store = BuildRMQStore().Pass();
+ rmq_store->Load(base::Bind(&RMQStoreTest::LoadCallback,
+ base::Unretained(this),
+ &load_result));
+ PumpLoop();
+
+ ASSERT_EQ(persistent_ids, load_result.incoming_messages);
+ ASSERT_TRUE(load_result.outgoing_messages.empty());
+
+ rmq_store->RemoveIncomingMessages(persistent_ids,
+ base::Bind(&RMQStoreTest::UpdateCallback,
+ base::Unretained(this)));
+ PumpLoop();
+
+ rmq_store = BuildRMQStore().Pass();
+ load_result.incoming_messages.clear();
+ rmq_store->Load(base::Bind(&RMQStoreTest::LoadCallback,
+ base::Unretained(this),
+ &load_result));
+ PumpLoop();
+
+ ASSERT_TRUE(load_result.incoming_messages.empty());
+ ASSERT_TRUE(load_result.outgoing_messages.empty());
+}
+
+// Verify saving some outgoing messages, reopening the directory, and then
+// removing those outgoing messages.
+TEST_F(RMQStoreTest, OutgoingMessages) {
+ scoped_ptr<RMQStore> rmq_store(BuildRMQStore());
+ RMQStore::LoadResult load_result;
+ rmq_store->Load(base::Bind(&RMQStoreTest::LoadCallback,
+ base::Unretained(this),
+ &load_result));
+ PumpLoop();
+
+ std::vector<std::string> persistent_ids;
+ const int kNumPersistentIds = 10;
+ for (int i = 0; i < kNumPersistentIds; ++i) {
+ persistent_ids.push_back(GetNextPersistentId());
+ mcs_proto::DataMessageStanza message;
+ message.set_from(persistent_ids.back());
+ message.set_category(persistent_ids.back());
+ rmq_store->AddOutgoingMessage(persistent_ids.back(),
+ MCSMessage(message),
+ base::Bind(&RMQStoreTest::UpdateCallback,
+ base::Unretained(this)));
+ PumpLoop();
+ }
+
+ rmq_store = BuildRMQStore().Pass();
+ rmq_store->Load(base::Bind(&RMQStoreTest::LoadCallback,
+ base::Unretained(this),
+ &load_result));
+ PumpLoop();
+
+ ASSERT_TRUE(load_result.incoming_messages.empty());
+ ASSERT_EQ(load_result.outgoing_messages.size(), persistent_ids.size());
+ for (int i =0 ; i < kNumPersistentIds; ++i) {
+ std::string id = persistent_ids[i];
+ ASSERT_TRUE(load_result.outgoing_messages[id]);
+ const mcs_proto::DataMessageStanza* message =
+ reinterpret_cast<mcs_proto::DataMessageStanza *>(
+ load_result.outgoing_messages[id]);
+ ASSERT_EQ(message->from(), id);
+ ASSERT_EQ(message->category(), id);
+ }
+
+ rmq_store->RemoveOutgoingMessages(persistent_ids,
+ base::Bind(&RMQStoreTest::UpdateCallback,
+ base::Unretained(this)));
+ PumpLoop();
+
+ rmq_store = BuildRMQStore().Pass();
+ load_result.outgoing_messages.clear();
+ rmq_store->Load(base::Bind(&RMQStoreTest::LoadCallback,
+ base::Unretained(this),
+ &load_result));
+ PumpLoop();
+
+ ASSERT_TRUE(load_result.incoming_messages.empty());
+ ASSERT_TRUE(load_result.outgoing_messages.empty());
+}
+
+// Verify incoming and outgoing messages don't conflict.
+TEST_F(RMQStoreTest, IncomingAndOutgoingMessages) {
+ scoped_ptr<RMQStore> rmq_store(BuildRMQStore());
+ RMQStore::LoadResult load_result;
+ rmq_store->Load(base::Bind(&RMQStoreTest::LoadCallback,
+ base::Unretained(this),
+ &load_result));
+ PumpLoop();
+
+ std::vector<std::string> persistent_ids;
+ const int kNumPersistentIds = 10;
+ for (int i = 0; i < kNumPersistentIds; ++i) {
+ persistent_ids.push_back(GetNextPersistentId());
+ rmq_store->AddIncomingMessage(persistent_ids.back(),
+ base::Bind(&RMQStoreTest::UpdateCallback,
+ base::Unretained(this)));
+ PumpLoop();
+
+ mcs_proto::DataMessageStanza message;
+ message.set_from(persistent_ids.back());
+ message.set_category(persistent_ids.back());
+ rmq_store->AddOutgoingMessage(persistent_ids.back(),
+ MCSMessage(message),
+ base::Bind(&RMQStoreTest::UpdateCallback,
+ base::Unretained(this)));
+ PumpLoop();
+ }
+
+
+ rmq_store = BuildRMQStore().Pass();
+ rmq_store->Load(base::Bind(&RMQStoreTest::LoadCallback,
+ base::Unretained(this),
+ &load_result));
+ PumpLoop();
+
+ ASSERT_EQ(persistent_ids, load_result.incoming_messages);
+ ASSERT_EQ(load_result.outgoing_messages.size(), persistent_ids.size());
+ for (int i =0 ; i < kNumPersistentIds; ++i) {
+ std::string id = persistent_ids[i];
+ ASSERT_TRUE(load_result.outgoing_messages[id]);
+ const mcs_proto::DataMessageStanza* message =
+ reinterpret_cast<mcs_proto::DataMessageStanza *>(
+ load_result.outgoing_messages[id]);
+ ASSERT_EQ(message->from(), id);
+ ASSERT_EQ(message->category(), id);
+ }
+
+ rmq_store->RemoveIncomingMessages(persistent_ids,
+ base::Bind(&RMQStoreTest::UpdateCallback,
+ base::Unretained(this)));
+ PumpLoop();
+ rmq_store->RemoveOutgoingMessages(persistent_ids,
+ base::Bind(&RMQStoreTest::UpdateCallback,
+ base::Unretained(this)));
+ PumpLoop();
+
+ rmq_store = BuildRMQStore().Pass();
+ load_result.incoming_messages.clear();
+ load_result.outgoing_messages.clear();
+ rmq_store->Load(base::Bind(&RMQStoreTest::LoadCallback,
+ base::Unretained(this),
+ &load_result));
+ PumpLoop();
+
+ ASSERT_TRUE(load_result.incoming_messages.empty());
+ ASSERT_TRUE(load_result.outgoing_messages.empty());
+}
+
+} // namespace
+
+} // namespace gcm
diff --git a/chromium/google_apis/gcm/gcm.gyp b/chromium/google_apis/gcm/gcm.gyp
new file mode 100644
index 00000000000..f81c4ef4493
--- /dev/null
+++ b/chromium/google_apis/gcm/gcm.gyp
@@ -0,0 +1,126 @@
+# Copyright 2013 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+{
+ 'variables': {
+ 'chromium_code': 1,
+ },
+
+ 'targets': [
+ # The public GCM target.
+ {
+ 'target_name': 'gcm',
+ 'type': '<(component)',
+ 'variables': {
+ 'enable_wexit_time_destructors': 1,
+ 'proto_in_dir': './protocol',
+ 'proto_out_dir': 'google_apis/gcm/protocol',
+ 'cc_generator_options': 'dllexport_decl=GCM_EXPORT:',
+ 'cc_include': 'google_apis/gcm/base/gcm_export.h',
+ },
+ 'include_dirs': [
+ '../..',
+ ],
+ 'defines': [
+ 'GCM_IMPLEMENTATION',
+ ],
+ 'export_dependent_settings': [
+ '../../third_party/protobuf/protobuf.gyp:protobuf_lite'
+ ],
+ 'dependencies': [
+ '../../base/base.gyp:base',
+ '../../base/third_party/dynamic_annotations/dynamic_annotations.gyp:dynamic_annotations',
+ '../../components/components.gyp:encryptor',
+ '../../net/net.gyp:net',
+ '../../third_party/leveldatabase/leveldatabase.gyp:leveldatabase',
+ '../../third_party/protobuf/protobuf.gyp:protobuf_lite',
+ '../../url/url.gyp:url_lib',
+ ],
+ 'sources': [
+ 'base/mcs_message.h',
+ 'base/mcs_message.cc',
+ 'base/mcs_util.h',
+ 'base/mcs_util.cc',
+ 'base/socket_stream.h',
+ 'base/socket_stream.cc',
+ 'engine/connection_factory.h',
+ 'engine/connection_factory.cc',
+ 'engine/connection_factory_impl.h',
+ 'engine/connection_factory_impl.cc',
+ 'engine/connection_handler.h',
+ 'engine/connection_handler.cc',
+ 'engine/connection_handler_impl.h',
+ 'engine/connection_handler_impl.cc',
+ 'engine/mcs_client.h',
+ 'engine/mcs_client.cc',
+ 'engine/rmq_store.h',
+ 'engine/rmq_store.cc',
+ 'gcm_client.cc',
+ 'gcm_client.h',
+ 'gcm_client_impl.cc',
+ 'gcm_client_impl.h',
+ 'protocol/mcs.proto',
+ ],
+ 'includes': [
+ '../../build/protoc.gypi'
+ ],
+ },
+
+ # A standalone MCS (mobile connection server) client.
+ {
+ 'target_name': 'mcs_probe',
+ 'type': 'executable',
+ 'variables': { 'enable_wexit_time_destructors': 1, },
+ 'include_dirs': [
+ '../..',
+ ],
+ 'dependencies': [
+ '../../base/base.gyp:base',
+ '../../net/net.gyp:net',
+ '../../net/net.gyp:net_test_support',
+ '../../third_party/protobuf/protobuf.gyp:protobuf_lite',
+ 'gcm'
+ ],
+ 'sources': [
+ 'tools/mcs_probe.cc',
+ ],
+ },
+
+ # The main GCM unit tests.
+ {
+ 'target_name': 'gcm_unit_tests',
+ 'type': '<(gtest_target_type)',
+ 'variables': { 'enable_wexit_time_destructors': 1, },
+ 'include_dirs': [
+ '../..',
+ ],
+ 'export_dependent_settings': [
+ '../../third_party/protobuf/protobuf.gyp:protobuf_lite'
+ ],
+ 'dependencies': [
+ '../../base/base.gyp:run_all_unittests',
+ '../../base/base.gyp:base',
+ '../../components/components.gyp:encryptor',
+ '../../net/net.gyp:net',
+ '../../net/net.gyp:net_test_support',
+ '../../testing/gtest.gyp:gtest',
+ '../../third_party/protobuf/protobuf.gyp:protobuf_lite',
+ 'gcm'
+ ],
+ 'sources': [
+ 'base/mcs_message_unittest.cc',
+ 'base/mcs_util_unittest.cc',
+ 'base/socket_stream_unittest.cc',
+ 'engine/connection_factory_impl_unittest.cc',
+ 'engine/connection_handler_impl_unittest.cc',
+ 'engine/fake_connection_factory.h',
+ 'engine/fake_connection_factory.cc',
+ 'engine/fake_connection_handler.h',
+ 'engine/fake_connection_handler.cc',
+ 'engine/mcs_client_unittest.cc',
+ 'engine/rmq_store_unittest.cc',
+ ]
+ },
+ ],
+}
diff --git a/chromium/google_apis/gcm/gcm_client.cc b/chromium/google_apis/gcm/gcm_client.cc
new file mode 100644
index 00000000000..e437a305e47
--- /dev/null
+++ b/chromium/google_apis/gcm/gcm_client.cc
@@ -0,0 +1,45 @@
+// Copyright 2013 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "google_apis/gcm/gcm_client.h"
+
+#include "base/lazy_instance.h"
+#include "google_apis/gcm/gcm_client_impl.h"
+
+namespace gcm {
+
+namespace {
+
+static base::LazyInstance<GCMClientImpl>::Leaky g_gcm_client =
+ LAZY_INSTANCE_INITIALIZER;
+static GCMClient* g_gcm_client_override = NULL;
+
+} // namespace
+
+GCMClient::OutgoingMessage::OutgoingMessage()
+ : time_to_live(0) {
+}
+
+GCMClient::OutgoingMessage::~OutgoingMessage() {
+}
+
+GCMClient::IncomingMessage::IncomingMessage() {
+}
+
+GCMClient::IncomingMessage::~IncomingMessage() {
+}
+
+// static
+GCMClient* GCMClient::Get() {
+ if (g_gcm_client_override)
+ return g_gcm_client_override;
+ return g_gcm_client.Pointer();
+}
+
+// static
+void GCMClient::SetForTesting(GCMClient* client) {
+ g_gcm_client_override = client;
+}
+
+} // namespace gcm
diff --git a/chromium/google_apis/gcm/gcm_client.h b/chromium/google_apis/gcm/gcm_client.h
new file mode 100644
index 00000000000..66e17a04489
--- /dev/null
+++ b/chromium/google_apis/gcm/gcm_client.h
@@ -0,0 +1,193 @@
+// Copyright 2013 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef GOOGLE_APIS_GCM_GCM_CLIENT_H_
+#define GOOGLE_APIS_GCM_GCM_CLIENT_H_
+
+#include <map>
+#include <string>
+#include <vector>
+
+#include "base/basictypes.h"
+#include "google_apis/gcm/base/gcm_export.h"
+
+namespace base {
+class TaskRunner;
+}
+
+namespace gcm {
+
+// Interface that encapsulates the network communications with the Google Cloud
+// Messaging server. This interface is not supposed to be thread-safe.
+class GCM_EXPORT GCMClient {
+ public:
+ enum Result {
+ // Successful operation.
+ SUCCESS,
+ // Invalid parameter.
+ INVALID_PARAMETER,
+ // Previous asynchronous operation is still pending to finish. Certain
+ // operation, like register, is only allowed one at a time.
+ ASYNC_OPERATION_PENDING,
+ // Network socket error.
+ NETWORK_ERROR,
+ // Problem at the server.
+ SERVER_ERROR,
+ // Exceeded the specified TTL during message sending.
+ TTL_EXCEEDED,
+ // Other errors.
+ UNKNOWN_ERROR
+ };
+
+ // Message data consisting of key-value pairs.
+ typedef std::map<std::string, std::string> MessageData;
+
+ // Message to be delivered to the other party.
+ struct GCM_EXPORT OutgoingMessage {
+ OutgoingMessage();
+ ~OutgoingMessage();
+
+ // Message ID.
+ std::string id;
+ // In seconds.
+ int time_to_live;
+ MessageData data;
+ };
+
+ // Message being received from the other party.
+ struct GCM_EXPORT IncomingMessage {
+ IncomingMessage();
+ ~IncomingMessage();
+
+ MessageData data;
+ };
+
+ // The check-in info for the user. Returned by the server.
+ struct GCM_EXPORT CheckInInfo {
+ CheckInInfo() : android_id(0), secret(0) {}
+ bool IsValid() const { return android_id != 0 && secret != 0; }
+ void Reset() {
+ android_id = 0;
+ secret = 0;
+ }
+
+ uint64 android_id;
+ uint64 secret;
+ };
+
+ // A delegate interface that allows the GCMClient instance to interact with
+ // its caller, i.e. notifying asynchronous event.
+ class Delegate {
+ public:
+ // Called when the user has been checked in successfully or an error occurs.
+ // |checkin_info|: valid if the checkin completed successfully.
+ // |result|: the type of the error if an error occured, success otherwise.
+ virtual void OnCheckInFinished(const CheckInInfo& checkin_info,
+ Result result) = 0;
+
+ // Called when the registration completed successfully or an error occurs.
+ // |app_id|: application ID.
+ // |registration_id|: non-empty if the registration completed successfully.
+ // |result|: the type of the error if an error occured, success otherwise.
+ virtual void OnRegisterFinished(const std::string& app_id,
+ const std::string& registration_id,
+ Result result) = 0;
+
+ // Called when the message is scheduled to send successfully or an error
+ // occurs.
+ // |app_id|: application ID.
+ // |message_id|: ID of the message being sent.
+ // |result|: the type of the error if an error occured, success otherwise.
+ virtual void OnSendFinished(const std::string& app_id,
+ const std::string& message_id,
+ Result result) = 0;
+
+ // Called when a message has been received.
+ // |app_id|: application ID.
+ // |message|: message received.
+ virtual void OnMessageReceived(const std::string& app_id,
+ const IncomingMessage& message) = 0;
+
+ // Called when some messages have been deleted from the server.
+ // |app_id|: application ID.
+ virtual void OnMessagesDeleted(const std::string& app_id) = 0;
+
+ // Called when a message failed to send to the server.
+ // |app_id|: application ID.
+ // |message_id|: ID of the message being sent.
+ // |result|: the type of the error if an error occured, success otherwise.
+ virtual void OnMessageSendError(const std::string& app_id,
+ const std::string& message_id,
+ Result result) = 0;
+
+ // Returns the checkin info associated with this user. The delegate class
+ // is expected to persist the checkin info that is provided by
+ // OnCheckInFinished.
+ virtual CheckInInfo GetCheckInInfo() const = 0;
+
+ // Called when the loading from the persistent store is done. The loading
+ // is triggered asynchronously when GCMClient is created.
+ virtual void OnLoadingCompleted() = 0;
+
+ // Returns a task runner for file operations that may block. This is used
+ // in writing to or reading from the persistent store.
+ virtual base::TaskRunner* GetFileTaskRunner() = 0;
+ };
+
+ // Returns the single instance. Multiple profiles share the same client
+ // that makes use of the same MCS connection.
+ static GCMClient* Get();
+
+ // Passes a mocked instance for testing purpose.
+ static void SetForTesting(GCMClient* client);
+
+ // Checks in the user to use GCM. If the device has not been checked in, it
+ // will be done first.
+ // |username|: the username (email address) used to check in with the server.
+ // |delegate|: the delegate whose methods will be called asynchronously in
+ // response to events and messages.
+ virtual void CheckIn(const std::string& username, Delegate* delegate) = 0;
+
+ // Registers the application for GCM. Delegate::OnRegisterFinished will be
+ // called asynchronously upon completion.
+ // |username|: the username (email address) passed in CheckIn.
+ // |app_id|: application ID.
+ // |cert|: SHA-1 of public key of the application, in base16 format.
+ // |sender_ids|: list of IDs of the servers that are allowed to send the
+ // messages to the application. These IDs are assigned by the
+ // Google API Console.
+ virtual void Register(const std::string& username,
+ const std::string& app_id,
+ const std::string& cert,
+ const std::vector<std::string>& sender_ids) = 0;
+
+ // Unregisters the application from GCM when it is uninstalled.
+ // Delegate::OnUnregisterFinished will be called asynchronously upon
+ // completion.
+ // |username|: the username (email address) passed in CheckIn.
+ // |app_id|: application ID.
+ virtual void Unregister(const std::string& username,
+ const std::string& app_id) = 0;
+
+ // Sends a message to a given receiver. Delegate::OnSendFinished will be
+ // called asynchronously upon completion.
+ // |username|: the username (email address) passed in CheckIn.
+ // |app_id|: application ID.
+ // |receiver_id|: registration ID of the receiver party.
+ // |message|: message to be sent.
+ virtual void Send(const std::string& username,
+ const std::string& app_id,
+ const std::string& receiver_id,
+ const OutgoingMessage& message) = 0;
+
+ // Returns true if the loading from the persistent store is still in progress.
+ virtual bool IsLoading() const = 0;
+
+ protected:
+ virtual ~GCMClient() {}
+};
+
+} // namespace gcm
+
+#endif // GOOGLE_APIS_GCM_GCM_CLIENT_H_
diff --git a/chromium/google_apis/gcm/gcm_client_impl.cc b/chromium/google_apis/gcm/gcm_client_impl.cc
new file mode 100644
index 00000000000..76900cda747
--- /dev/null
+++ b/chromium/google_apis/gcm/gcm_client_impl.cc
@@ -0,0 +1,39 @@
+// Copyright 2013 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "google_apis/gcm/gcm_client_impl.h"
+
+namespace gcm {
+
+GCMClientImpl::GCMClientImpl() {
+}
+
+GCMClientImpl::~GCMClientImpl() {
+}
+
+void GCMClientImpl::CheckIn(const std::string& username,
+ Delegate* delegate) {
+}
+
+void GCMClientImpl::Register(const std::string& username,
+ const std::string& app_id,
+ const std::string& cert,
+ const std::vector<std::string>& sender_ids) {
+}
+
+void GCMClientImpl::Unregister(const std::string& username,
+ const std::string& app_id) {
+}
+
+void GCMClientImpl::Send(const std::string& username,
+ const std::string& app_id,
+ const std::string& receiver_id,
+ const OutgoingMessage& message) {
+}
+
+bool GCMClientImpl::IsLoading() const {
+ return false;
+}
+
+} // namespace gcm
diff --git a/chromium/google_apis/gcm/gcm_client_impl.h b/chromium/google_apis/gcm/gcm_client_impl.h
new file mode 100644
index 00000000000..46f910e546a
--- /dev/null
+++ b/chromium/google_apis/gcm/gcm_client_impl.h
@@ -0,0 +1,39 @@
+// Copyright 2013 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef GOOGLE_APIS_GCM_GCM_CLIENT_IMPL_H_
+#define GOOGLE_APIS_GCM_GCM_CLIENT_IMPL_H_
+
+#include "base/compiler_specific.h"
+#include "google_apis/gcm/gcm_client.h"
+
+namespace gcm {
+
+class GCMClientImpl : public GCMClient {
+ public:
+ GCMClientImpl();
+ virtual ~GCMClientImpl();
+
+ // Overridden from GCMClient:
+ virtual void CheckIn(const std::string& username,
+ Delegate* delegate) OVERRIDE;
+ virtual void Register(const std::string& username,
+ const std::string& app_id,
+ const std::string& cert,
+ const std::vector<std::string>& sender_ids) OVERRIDE;
+ virtual void Unregister(const std::string& username,
+ const std::string& app_id) OVERRIDE;
+ virtual void Send(const std::string& username,
+ const std::string& app_id,
+ const std::string& receiver_id,
+ const OutgoingMessage& message) OVERRIDE;
+ virtual bool IsLoading() const OVERRIDE;
+
+ private:
+ DISALLOW_COPY_AND_ASSIGN(GCMClientImpl);
+};
+
+} // namespace gcm
+
+#endif // GOOGLE_APIS_GCM_GCM_CLIENT_IMPL_H_
diff --git a/chromium/google_apis/gcm/protocol/mcs.proto b/chromium/google_apis/gcm/protocol/mcs.proto
new file mode 100644
index 00000000000..2926d1037f3
--- /dev/null
+++ b/chromium/google_apis/gcm/protocol/mcs.proto
@@ -0,0 +1,269 @@
+// Copyright 2013 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+//
+// MCS protocol for communication between Chrome client and Mobile Connection
+// Server .
+
+syntax = "proto2";
+
+option optimize_for = LITE_RUNTIME;
+option retain_unknown_fields = true;
+
+package mcs_proto;
+
+/*
+ Common fields/comments:
+
+ stream_id: no longer sent by server, each side keeps a counter
+ last_stream_id_received: sent only if a packet was received since last time
+ a last_stream was sent
+ status: new bitmask including the 'idle' as bit 0.
+
+ */
+
+/**
+ TAG: 0
+ */
+message HeartbeatPing {
+ optional int32 stream_id = 1;
+ optional int32 last_stream_id_received = 2;
+ optional int64 status = 3;
+}
+
+/**
+ TAG: 1
+ */
+message HeartbeatAck {
+ optional int32 stream_id = 1;
+ optional int32 last_stream_id_received = 2;
+ optional int64 status = 3;
+}
+
+message ErrorInfo {
+ required int32 code = 1;
+ optional string message = 2;
+ optional string type = 3;
+ optional Extension extension = 4;
+}
+
+// MobileSettings class.
+// "u:f", "u:b", "u:s" - multi user devices reporting foreground, background
+// and stopped users.
+// hbping: heatbeat ping interval
+// rmq2v: include explicit stream IDs
+
+message Setting {
+ required string name = 1;
+ required string value = 2;
+}
+
+message HeartbeatStat {
+ required string ip = 1;
+ required bool timeout = 2;
+ required int32 interval_ms = 3;
+}
+
+message HeartbeatConfig {
+ optional bool upload_stat = 1;
+ optional string ip = 2;
+ optional int32 interval_ms = 3;
+}
+
+/**
+ TAG: 2
+ */
+message LoginRequest {
+ enum AuthService {
+ ANDROID_ID = 2;
+ }
+ required string id = 1; // Must be present ( proto required ), may be empty
+ // string.
+ // mcs.android.com.
+ required string domain = 2;
+ // Decimal android ID
+ required string user = 3;
+
+ required string resource = 4;
+
+ // Secret
+ required string auth_token = 5;
+
+ // Format is: android-HEX_DEVICE_ID
+ // The user is the decimal value.
+ optional string device_id = 6;
+
+ // RMQ1 - no longer used
+ optional int64 last_rmq_id = 7;
+
+ repeated Setting setting = 8;
+ //optional int32 compress = 9;
+ repeated string received_persistent_id = 10;
+
+ // Replaced by "rmq2v" setting
+ // optional bool include_stream_ids = 11;
+
+ optional bool adaptive_heartbeat = 12;
+ optional HeartbeatStat heartbeat_stat = 13;
+ // Must be true.
+ optional bool use_rmq2 = 14;
+ optional int64 account_id = 15;
+
+ // ANDROID_ID = 2
+ optional AuthService auth_service = 16;
+
+ optional int32 network_type = 17;
+ optional int64 status = 18;
+}
+
+/**
+ * TAG: 3
+ */
+message LoginResponse {
+ required string id = 1;
+ // Not used.
+ optional string jid = 2;
+ // Null if login was ok.
+ optional ErrorInfo error = 3;
+ repeated Setting setting = 4;
+ optional int32 stream_id = 5;
+ // Should be "1"
+ optional int32 last_stream_id_received = 6;
+ optional HeartbeatConfig heartbeat_config = 7;
+ // used by the client to synchronize with the server timestamp.
+ optional int64 server_timestamp = 8;
+}
+
+message StreamErrorStanza {
+ required string type = 1;
+ optional string text = 2;
+}
+
+/**
+ * TAG: 4
+ */
+message Close {
+}
+
+message Extension {
+ // 12: SelectiveAck
+ // 13: StreamAck
+ required int32 id = 1;
+ required bytes data = 2;
+}
+
+/**
+ * TAG: 7
+ * IqRequest must contain a single extension. IqResponse may contain 0 or 1
+ * extensions.
+ */
+message IqStanza {
+ enum IqType {
+ GET = 0;
+ SET = 1;
+ RESULT = 2;
+ IQ_ERROR = 3;
+ }
+
+ optional int64 rmq_id = 1;
+ required IqType type = 2;
+ required string id = 3;
+ optional string from = 4;
+ optional string to = 5;
+ optional ErrorInfo error = 6;
+
+ // Only field used in the 38+ protocol (besides common last_stream_id_received, status, rmq_id)
+ optional Extension extension = 7;
+
+ optional string persistent_id = 8;
+ optional int32 stream_id = 9;
+ optional int32 last_stream_id_received = 10;
+ optional int64 account_id = 11;
+ optional int64 status = 12;
+}
+
+message AppData {
+ required string key = 1;
+ required string value = 2;
+}
+
+/**
+ * TAG: 8
+ */
+message DataMessageStanza {
+ // Not used.
+ // optional int64 rmq_id = 1;
+
+ // This is the message ID, set by client, DMP.9 (message_id)
+ optional string id = 2;
+
+ // Project ID of the sender, DMP.1
+ required string from = 3;
+
+ // Part of DMRequest - also the key in DataMessageProto.
+ optional string to = 4;
+
+ // Package name. DMP.2
+ required string category = 5;
+
+ // The collapsed key, DMP.3
+ optional string token = 6;
+
+ // User data + GOOGLE. prefixed special entries, DMP.4
+ repeated AppData app_data = 7;
+
+ // Not used.
+ optional bool from_trusted_server = 8;
+
+ // Part of the ACK protocol, returned in DataMessageResponse on server side.
+ // It's part of the key of DMP.
+ optional string persistent_id = 9;
+
+ // In-stream ack. Increments on each message sent - a bit redundant
+ // Not used in DMP/DMR.
+ optional int32 stream_id = 10;
+ optional int32 last_stream_id_received = 11;
+
+ // Not used.
+ // optional string permission = 12;
+
+ // Sent by the device shortly after registration.
+ optional string reg_id = 13;
+
+ // Not used.
+ // optional string pkg_signature = 14;
+ // Not used.
+ // optional string client_id = 15;
+
+ // serial number of the target user, DMP.8
+ // It is the 'serial number' according to user manager.
+ optional int64 device_user_id = 16;
+
+ // Time to live, in seconds.
+ optional int32 ttl = 17;
+ // Timestamp ( according to client ) when message was sent by app, in seconds
+ optional int64 sent = 18;
+
+ // How long has the message been queued before the flush, in seconds.
+ // This is needed to account for the time difference between server and
+ // client: server should adjust 'sent' based on his 'receive' time.
+ optional int32 queued = 19;
+
+ optional int64 status = 20;
+}
+
+/**
+ Included in IQ with ID 13, sent from client or server after 10 unconfirmed
+ messages.
+ */
+message StreamAck {
+ // No last_streamid_received required. This is included within an IqStanza,
+ // which includes the last_stream_id_received.
+}
+
+/**
+ Included in IQ sent after LoginResponse from server with ID 12.
+*/
+message SelectiveAck {
+ repeated string id = 1;
+}
diff --git a/chromium/google_apis/gcm/tools/mcs_probe.cc b/chromium/google_apis/gcm/tools/mcs_probe.cc
new file mode 100644
index 00000000000..bc4ad7cad3a
--- /dev/null
+++ b/chromium/google_apis/gcm/tools/mcs_probe.cc
@@ -0,0 +1,372 @@
+// Copyright 2013 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+//
+// A standalone tool for testing MCS connections and the MCS client on their
+// own.
+
+#include <cstddef>
+#include <cstdio>
+#include <string>
+
+#include "base/at_exit.h"
+#include "base/command_line.h"
+#include "base/compiler_specific.h"
+#include "base/logging.h"
+#include "base/memory/ref_counted.h"
+#include "base/memory/scoped_ptr.h"
+#include "base/message_loop/message_loop.h"
+#include "base/run_loop.h"
+#include "base/strings/string_number_conversions.h"
+#include "base/threading/thread.h"
+#include "base/threading/worker_pool.h"
+#include "base/values.h"
+#include "google_apis/gcm/base/mcs_message.h"
+#include "google_apis/gcm/base/mcs_util.h"
+#include "google_apis/gcm/engine/connection_factory_impl.h"
+#include "google_apis/gcm/engine/mcs_client.h"
+#include "net/base/host_mapping_rules.h"
+#include "net/base/net_log_logger.h"
+#include "net/cert/cert_verifier.h"
+#include "net/dns/host_resolver.h"
+#include "net/http/http_auth_handler_factory.h"
+#include "net/http/http_network_session.h"
+#include "net/http/http_server_properties_impl.h"
+#include "net/http/transport_security_state.h"
+#include "net/socket/client_socket_factory.h"
+#include "net/socket/ssl_client_socket.h"
+#include "net/ssl/default_server_bound_cert_store.h"
+#include "net/ssl/server_bound_cert_service.h"
+#include "net/url_request/url_request_test_util.h"
+
+#if defined(OS_MACOSX)
+#include "base/mac/scoped_nsautorelease_pool.h"
+#endif
+
+// This is a simple utility that initializes an mcs client and
+// prints out any events.
+namespace gcm {
+namespace {
+
+// The default server to communicate with.
+const char kMCSServerHost[] = "mtalk.google.com";
+const uint16 kMCSServerPort = 5228;
+
+// Command line switches.
+const char kRMQFileName[] = "rmq_file";
+const char kAndroidIdSwitch[] = "android_id";
+const char kSecretSwitch[] = "secret";
+const char kLogFileSwitch[] = "log-file";
+const char kIgnoreCertSwitch[] = "ignore-certs";
+const char kServerHostSwitch[] = "host";
+const char kServerPortSwitch[] = "port";
+
+void MessageReceivedCallback(const MCSMessage& message) {
+ LOG(INFO) << "Received message with id "
+ << GetPersistentId(message.GetProtobuf()) << " and tag "
+ << static_cast<int>(message.tag());
+
+ if (message.tag() == kDataMessageStanzaTag) {
+ const mcs_proto::DataMessageStanza& data_message =
+ reinterpret_cast<const mcs_proto::DataMessageStanza&>(
+ message.GetProtobuf());
+ DVLOG(1) << " to: " << data_message.to();
+ DVLOG(1) << " from: " << data_message.from();
+ DVLOG(1) << " category: " << data_message.category();
+ DVLOG(1) << " sent: " << data_message.sent();
+ for (int i = 0; i < data_message.app_data_size(); ++i) {
+ DVLOG(1) << " App data " << i << " "
+ << data_message.app_data(i).key() << " : "
+ << data_message.app_data(i).value();
+ }
+ }
+}
+
+void MessageSentCallback(const std::string& local_id) {
+ LOG(INFO) << "Message sent. Status: " << local_id;
+}
+
+// Needed to use a real host resolver.
+class MyTestURLRequestContext : public net::TestURLRequestContext {
+ public:
+ MyTestURLRequestContext() : TestURLRequestContext(true) {
+ context_storage_.set_host_resolver(
+ net::HostResolver::CreateDefaultResolver(NULL));
+ context_storage_.set_transport_security_state(
+ new net::TransportSecurityState());
+ Init();
+ }
+
+ virtual ~MyTestURLRequestContext() {}
+};
+
+class MyTestURLRequestContextGetter : public net::TestURLRequestContextGetter {
+ public:
+ explicit MyTestURLRequestContextGetter(
+ const scoped_refptr<base::MessageLoopProxy>& io_message_loop_proxy)
+ : TestURLRequestContextGetter(io_message_loop_proxy) {}
+
+ virtual net::TestURLRequestContext* GetURLRequestContext() OVERRIDE {
+ // Construct |context_| lazily so it gets constructed on the right
+ // thread (the IO thread).
+ if (!context_)
+ context_.reset(new MyTestURLRequestContext());
+ return context_.get();
+ }
+
+ private:
+ virtual ~MyTestURLRequestContextGetter() {}
+
+ scoped_ptr<MyTestURLRequestContext> context_;
+};
+
+// A net log that logs all events by default.
+class MyTestNetLog : public net::NetLog {
+ public:
+ MyTestNetLog() {
+ SetBaseLogLevel(LOG_ALL);
+ }
+ virtual ~MyTestNetLog() {}
+};
+
+// A cert verifier that access all certificates.
+class MyTestCertVerifier : public net::CertVerifier {
+ public:
+ MyTestCertVerifier() {}
+ virtual ~MyTestCertVerifier() {}
+
+ virtual int Verify(net::X509Certificate* cert,
+ const std::string& hostname,
+ int flags,
+ net::CRLSet* crl_set,
+ net::CertVerifyResult* verify_result,
+ const net::CompletionCallback& callback,
+ RequestHandle* out_req,
+ const net::BoundNetLog& net_log) OVERRIDE {
+ return net::OK;
+ }
+
+ virtual void CancelRequest(RequestHandle req) OVERRIDE {
+ // Do nothing.
+ }
+};
+
+class MCSProbe {
+ public:
+ MCSProbe(
+ const CommandLine& command_line,
+ scoped_refptr<net::URLRequestContextGetter> url_request_context_getter);
+ ~MCSProbe();
+
+ void Start();
+
+ uint64 android_id() const { return android_id_; }
+ uint64 secret() const { return secret_; }
+
+ private:
+ void InitializeNetworkState();
+ void BuildNetworkSession();
+
+ void InitializationCallback(bool success,
+ uint64 restored_android_id,
+ uint64 restored_security_token);
+
+ CommandLine command_line_;
+
+ base::FilePath rmq_path_;
+ uint64 android_id_;
+ uint64 secret_;
+ std::string server_host_;
+ int server_port_;
+
+ // Network state.
+ scoped_refptr<net::URLRequestContextGetter> url_request_context_getter_;
+ MyTestNetLog net_log_;
+ scoped_ptr<net::NetLogLogger> logger_;
+ scoped_ptr<base::Value> net_constants_;
+ scoped_ptr<net::HostResolver> host_resolver_;
+ scoped_ptr<net::CertVerifier> cert_verifier_;
+ scoped_ptr<net::ServerBoundCertService> system_server_bound_cert_service_;
+ scoped_ptr<net::TransportSecurityState> transport_security_state_;
+ scoped_ptr<net::URLSecurityManager> url_security_manager_;
+ scoped_ptr<net::HttpAuthHandlerFactory> http_auth_handler_factory_;
+ scoped_ptr<net::HttpServerPropertiesImpl> http_server_properties_;
+ scoped_ptr<net::HostMappingRules> host_mapping_rules_;
+ scoped_refptr<net::HttpNetworkSession> network_session_;
+ scoped_ptr<net::ProxyService> proxy_service_;
+
+ scoped_ptr<MCSClient> mcs_client_;
+
+ scoped_ptr<ConnectionFactoryImpl> connection_factory_;
+
+ base::Thread file_thread_;
+
+ scoped_ptr<base::RunLoop> run_loop_;
+};
+
+MCSProbe::MCSProbe(
+ const CommandLine& command_line,
+ scoped_refptr<net::URLRequestContextGetter> url_request_context_getter)
+ : command_line_(command_line),
+ rmq_path_(base::FilePath(FILE_PATH_LITERAL("gcm_rmq_store"))),
+ android_id_(0),
+ secret_(0),
+ server_port_(0),
+ url_request_context_getter_(url_request_context_getter),
+ file_thread_("FileThread") {
+ if (command_line.HasSwitch(kRMQFileName)) {
+ rmq_path_ = command_line.GetSwitchValuePath(kRMQFileName);
+ }
+ if (command_line.HasSwitch(kAndroidIdSwitch)) {
+ base::StringToUint64(command_line.GetSwitchValueASCII(kAndroidIdSwitch),
+ &android_id_);
+ }
+ if (command_line.HasSwitch(kSecretSwitch)) {
+ base::StringToUint64(command_line.GetSwitchValueASCII(kSecretSwitch),
+ &secret_);
+ }
+ server_host_ = kMCSServerHost;
+ if (command_line.HasSwitch(kServerHostSwitch)) {
+ server_host_ = command_line.GetSwitchValueASCII(kServerHostSwitch);
+ }
+ server_port_ = kMCSServerPort;
+ if (command_line.HasSwitch(kServerPortSwitch)) {
+ base::StringToInt(command_line.GetSwitchValueASCII(kServerPortSwitch),
+ &server_port_);
+ }
+}
+
+MCSProbe::~MCSProbe() {
+ file_thread_.Stop();
+}
+
+void MCSProbe::Start() {
+ file_thread_.Start();
+ InitializeNetworkState();
+ BuildNetworkSession();
+ connection_factory_.reset(
+ new ConnectionFactoryImpl(GURL("https://" + net::HostPortPair(
+ server_host_, server_port_).ToString()),
+ network_session_,
+ &net_log_));
+ mcs_client_.reset(new MCSClient(rmq_path_,
+ connection_factory_.get(),
+ file_thread_.message_loop_proxy()));
+ run_loop_.reset(new base::RunLoop());
+ mcs_client_->Initialize(base::Bind(&MCSProbe::InitializationCallback,
+ base::Unretained(this)),
+ base::Bind(&MessageReceivedCallback),
+ base::Bind(&MessageSentCallback));
+ run_loop_->Run();
+}
+
+void MCSProbe::InitializeNetworkState() {
+ FILE* log_file = NULL;
+ if (command_line_.HasSwitch(kLogFileSwitch)) {
+ base::FilePath log_path = command_line_.GetSwitchValuePath(kLogFileSwitch);
+#if defined(OS_WIN)
+ log_file = _wfopen(log_path.value().c_str(), L"w");
+#elif defined(OS_POSIX)
+ log_file = fopen(log_path.value().c_str(), "w");
+#endif
+ }
+ net_constants_.reset(net::NetLogLogger::GetConstants());
+ if (log_file != NULL) {
+ logger_.reset(new net::NetLogLogger(log_file, *net_constants_));
+ logger_->StartObserving(&net_log_);
+ }
+
+ host_resolver_ = net::HostResolver::CreateDefaultResolver(&net_log_);
+
+ if (command_line_.HasSwitch(kIgnoreCertSwitch)) {
+ cert_verifier_.reset(new MyTestCertVerifier());
+ } else {
+ cert_verifier_.reset(net::CertVerifier::CreateDefault());
+ }
+ system_server_bound_cert_service_.reset(
+ new net::ServerBoundCertService(
+ new net::DefaultServerBoundCertStore(NULL),
+ base::WorkerPool::GetTaskRunner(true)));
+
+ transport_security_state_.reset(new net::TransportSecurityState());
+ url_security_manager_.reset(net::URLSecurityManager::Create(NULL, NULL));
+ http_auth_handler_factory_.reset(
+ net::HttpAuthHandlerRegistryFactory::Create(
+ std::vector<std::string>(1, "basic"),
+ url_security_manager_.get(),
+ host_resolver_.get(),
+ std::string(),
+ false,
+ false));
+ http_server_properties_.reset(new net::HttpServerPropertiesImpl());
+ host_mapping_rules_.reset(new net::HostMappingRules());
+ proxy_service_.reset(net::ProxyService::CreateDirectWithNetLog(&net_log_));
+}
+
+void MCSProbe::BuildNetworkSession() {
+ net::HttpNetworkSession::Params session_params;
+ session_params.host_resolver = host_resolver_.get();
+ session_params.cert_verifier = cert_verifier_.get();
+ session_params.server_bound_cert_service =
+ system_server_bound_cert_service_.get();
+ session_params.transport_security_state = transport_security_state_.get();
+ session_params.ssl_config_service = new net::SSLConfigServiceDefaults();
+ session_params.http_auth_handler_factory = http_auth_handler_factory_.get();
+ session_params.http_server_properties =
+ http_server_properties_->GetWeakPtr();
+ session_params.network_delegate = NULL; // TODO(zea): implement?
+ session_params.host_mapping_rules = host_mapping_rules_.get();
+ session_params.ignore_certificate_errors = true;
+ session_params.http_pipelining_enabled = false;
+ session_params.testing_fixed_http_port = 0;
+ session_params.testing_fixed_https_port = 0;
+ session_params.net_log = &net_log_;
+ session_params.proxy_service = proxy_service_.get();
+
+ network_session_ = new net::HttpNetworkSession(session_params);
+}
+
+void MCSProbe::InitializationCallback(bool success,
+ uint64 restored_android_id,
+ uint64 restored_security_token) {
+ LOG(INFO) << "Initialization " << (success ? "success!" : "failure!");
+ if (restored_android_id && restored_security_token) {
+ android_id_ = restored_android_id;
+ secret_ = restored_security_token;
+ }
+ if (success)
+ mcs_client_->Login(android_id_, secret_);
+}
+
+int MCSProbeMain(int argc, char* argv[]) {
+ base::AtExitManager exit_manager;
+
+ CommandLine::Init(argc, argv);
+ logging::LoggingSettings settings;
+ settings.logging_dest = logging::LOG_TO_SYSTEM_DEBUG_LOG;
+ logging::InitLogging(settings);
+
+ base::MessageLoopForIO message_loop;
+
+ // For check-in and creating registration ids.
+ const scoped_refptr<MyTestURLRequestContextGetter> context_getter =
+ new MyTestURLRequestContextGetter(
+ base::MessageLoop::current()->message_loop_proxy());
+
+ const CommandLine& command_line = *CommandLine::ForCurrentProcess();
+
+ MCSProbe mcs_probe(command_line, context_getter);
+ mcs_probe.Start();
+
+ base::RunLoop run_loop;
+ run_loop.Run();
+
+ return 0;
+}
+
+} // namespace
+} // namespace gcm
+
+int main(int argc, char* argv[]) {
+ return gcm::MCSProbeMain(argc, argv);
+}
diff --git a/chromium/google_apis/google_api_keys.cc b/chromium/google_apis/google_api_keys.cc
index 0baf00dc393..94f6812ab74 100644
--- a/chromium/google_apis/google_api_keys.cc
+++ b/chromium/google_apis/google_api_keys.cc
@@ -204,14 +204,14 @@ class APIKeyCache {
std::string temp;
if (environment->GetVar(environment_variable_name, &temp)) {
key_value = temp;
- LOG(INFO) << "Overriding API key " << environment_variable_name
- << " with value " << key_value << " from environment variable.";
+ VLOG(1) << "Overriding API key " << environment_variable_name
+ << " with value " << key_value << " from environment variable.";
}
if (command_line_switch && command_line->HasSwitch(command_line_switch)) {
key_value = command_line->GetSwitchValueASCII(command_line_switch);
- LOG(INFO) << "Overriding API key " << environment_variable_name
- << " with value " << key_value << " from command-line switch.";
+ VLOG(1) << "Overriding API key " << environment_variable_name
+ << " with value " << key_value << " from command-line switch.";
}
if (key_value == DUMMY_API_TOKEN) {
@@ -222,8 +222,8 @@ class APIKeyCache {
CHECK(false);
#endif
if (default_if_unset.size() > 0) {
- LOG(INFO) << "Using default value \"" << default_if_unset
- << "\" for API key " << environment_variable_name;
+ VLOG(1) << "Using default value \"" << default_if_unset
+ << "\" for API key " << environment_variable_name;
key_value = default_if_unset;
}
}
diff --git a/chromium/google_apis/google_apis.gyp b/chromium/google_apis/google_apis.gyp
index 762c3695847..2d281f2b2f3 100644
--- a/chromium/google_apis/google_apis.gyp
+++ b/chromium/google_apis/google_apis.gyp
@@ -20,6 +20,7 @@
'../base/base.gyp:base',
'../crypto/crypto.gyp:crypto',
'../net/net.gyp:net',
+ '../third_party/libxml/libxml.gyp:libxml',
],
'conditions': [
['google_api_key!=""', {
@@ -58,6 +59,38 @@
'cup/client_update_protocol.h',
'cup/client_update_protocol_nss.cc',
'cup/client_update_protocol_openssl.cc',
+ 'drive/auth_service.cc',
+ 'drive/auth_service.h',
+ 'drive/auth_service_interface.h',
+ 'drive/auth_service_observer.h',
+ 'drive/base_requests.cc',
+ 'drive/base_requests.h',
+ 'drive/drive_api_parser.cc',
+ 'drive/drive_api_parser.h',
+ 'drive/drive_api_requests.cc',
+ 'drive/drive_api_requests.h',
+ 'drive/drive_api_url_generator.cc',
+ 'drive/drive_api_url_generator.h',
+ 'drive/drive_common_callbacks.h',
+ 'drive/drive_entry_kinds.h',
+ 'drive/gdata_contacts_requests.cc',
+ 'drive/gdata_contacts_requests.h',
+ 'drive/gdata_errorcode.cc',
+ 'drive/gdata_errorcode.h',
+ 'drive/gdata_wapi_requests.cc',
+ 'drive/gdata_wapi_requests.h',
+ 'drive/gdata_wapi_parser.cc',
+ 'drive/gdata_wapi_parser.h',
+ 'drive/gdata_wapi_url_generator.cc',
+ 'drive/gdata_wapi_url_generator.h',
+ 'drive/request_sender.cc',
+ 'drive/request_sender.h',
+ 'drive/request_util.cc',
+ 'drive/request_util.h',
+ 'drive/task_util.cc',
+ 'drive/task_util.h',
+ 'drive/time_util.cc',
+ 'drive/time_util.h',
'gaia/gaia_auth_consumer.cc',
'gaia/gaia_auth_consumer.h',
'gaia/gaia_auth_fetcher.cc',